Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-documenter] Disambiguate output files with colliding names #1536

Merged
merged 5 commits into from
Sep 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { PackageName } from '@microsoft/node-core-library';
import { DocComment, DocInlineTag } from '@microsoft/tsdoc';
import { ApiModel, ApiItem, ApiItemKind, ApiDocumentedItem } from '@microsoft/api-extractor-model';

Expand Down Expand Up @@ -42,24 +41,20 @@ export class ExperimentalYamlDocumenter extends YamlDocumenter {
if (apiItem.kind === ApiItemKind.Namespace) {
// Namespaces don't have nodes yet
tocItem = {
name: apiItem.displayName
name: this._getTocItemName(apiItem)
};
} else {
if (this._shouldEmbed(apiItem.kind)) {
// Don't generate table of contents items for embedded definitions
continue;
}

if (apiItem.kind === ApiItemKind.Package) {
tocItem = {
name: PackageName.getUnscopedName(apiItem.displayName),
uid: this._getUid(apiItem)
};
} else {
tocItem = {
name: apiItem.displayName,
uid: this._getUid(apiItem)
};
tocItem = {
name: this._getTocItemName(apiItem),
uid: this._getUid(apiItem)
};

if (apiItem.kind !== ApiItemKind.Package) {
this._filterItem(apiItem, tocItem);
}
}
Expand Down
43 changes: 27 additions & 16 deletions apps/api-documenter/src/documenters/YamlDocumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export class YamlDocumenter {

private _apiItemsByCanonicalReference: Map<string, ApiItem>;
private _yamlReferences: IYamlReferences | undefined;

private _outputFolder: string;

public constructor(apiModel: ApiModel) {
Expand Down Expand Up @@ -232,25 +231,18 @@ export class YamlDocumenter {
if (apiItem.kind === ApiItemKind.Namespace) {
// Namespaces don't have nodes yet
tocItem = {
name: apiItem.displayName
name: this._getTocItemName(apiItem)
};
} else {
if (this._shouldEmbed(apiItem.kind)) {
// Don't generate table of contents items for embedded definitions
continue;
}

if (apiItem.kind === ApiItemKind.Package) {
tocItem = {
name: PackageName.getUnscopedName(apiItem.displayName),
uid: this._getUid(apiItem)
};
} else {
tocItem = {
name: apiItem.displayName,
uid: this._getUid(apiItem)
};
}
tocItem = {
name: this._getTocItemName(apiItem),
uid: this._getUid(apiItem)
};
}

tocItems.push(tocItem);
Expand All @@ -271,6 +263,20 @@ export class YamlDocumenter {
return tocItems;
}

/** @virtual */
protected _getTocItemName(apiItem: ApiItem): string {
let name: string = apiItem.displayName;
if (apiItem.kind === ApiItemKind.Package) {
name = PackageName.getUnscopedName(name);
}

if (apiItem.getMergedSiblings().length > 1) {
name += ` (${apiItem.kind})`;
}

return name;
}

protected _shouldEmbed(apiItemKind: ApiItemKind): boolean {
switch (apiItemKind) {
case ApiItemKind.Class:
Expand Down Expand Up @@ -591,7 +597,6 @@ export class YamlDocumenter {
*/
private _initApiItems(): void {
this._initApiItemsRecursive(this._apiModel);

}

/**
Expand Down Expand Up @@ -803,11 +808,17 @@ export class YamlDocumenter {
break;
}
}
return path.join(this._outputFolder, result + '.yml');

let disambiguator: string = '';
if (apiItem.getMergedSiblings().length > 1) {
disambiguator = `-${apiItem.kind.toLowerCase()}`;
}

return path.join(this._outputFolder, result + disambiguator + '.yml');
}

private _deleteOldOutputFiles(): void {
console.log('Deleting old output from ' + this._outputFolder);
FileSystem.ensureEmptyFolder(this._outputFolder);
}
}
}
19 changes: 19 additions & 0 deletions apps/api-extractor-model/src/items/ApiItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ApiPackage } from '../model/ApiPackage';
import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin';
import { DeserializerContext } from '../model/DeserializerContext';
import { InternalError } from '@microsoft/node-core-library';
import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin';

/**
* The type returned by the {@link ApiItem.kind} property, which can be used to easily distinguish subclasses of
Expand Down Expand Up @@ -173,6 +174,24 @@ export class ApiItem {
return [];
}

/**
* If this item has a name (i.e. extends `ApiNameMixin`), then return all items that have the same parent
* and the same name. Otherwise, return all items that have the same parent and the same `ApiItemKind`.
*
* @remarks
* Examples: For a function, this would return all overloads for the function. For a constructor, this would
* return all overloads for the constructor. For a merged declaration (e.g. a `namespace` and `enum` with the
* same name), this would return both declarations. If this item does not have a parent, or if it is the only
* item of its name/kind, then the result is an array containing only this item.
*/
public getMergedSiblings(): ReadonlyArray<ApiItem> {
const parent: ApiItem | undefined = this._parent;
if (parent && ApiItemContainerMixin.isBaseClassOf(parent)) {
return parent._getMergedSiblingsForMember(this);
}
return [];
}

/**
* Returns the chain of ancestors, starting from the root of the tree, and ending with the this item.
*/
Expand Down
77 changes: 65 additions & 12 deletions apps/api-extractor-model/src/mixins/ApiItemContainerMixin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.s

import { ApiItem, ApiItem_onParentChanged, IApiItemJson, IApiItemOptions, IApiItemConstructor } from '../items/ApiItem';
import {
ApiItem,
ApiItem_onParentChanged,
IApiItemJson,
IApiItemOptions,
IApiItemConstructor,
ApiItemKind
} from '../items/ApiItem';
import { ApiNameMixin } from './ApiNameMixin';
import { DeserializerContext } from '../model/DeserializerContext';
import { InternalError } from '@microsoft/node-core-library';

/**
* Constructor options for {@link (ApiItemContainerMixin:interface)}.
Expand All @@ -21,6 +29,7 @@ const _members: unique symbol = Symbol('ApiItemContainerMixin._members');
const _membersSorted: unique symbol = Symbol('ApiItemContainerMixin._membersSorted');
const _membersByContainerKey: unique symbol = Symbol('ApiItemContainerMixin._membersByContainerKey');
const _membersByName: unique symbol = Symbol('ApiItemContainerMixin._membersByName');
const _membersByKind: unique symbol = Symbol('ApiItemContainerMixin._membersByKind');

/**
* The mixin base class for API items that act as containers for other child items.
Expand Down Expand Up @@ -73,6 +82,12 @@ export interface ApiItemContainerMixin extends ApiItem {
*/
findMembersByName(name: string): ReadonlyArray<ApiItem>;

/**
* For a given member of this container, return its `ApiItem.getMergedSiblings()` list.
* @internal
*/
_getMergedSiblingsForMember(memberApiItem: ApiItem): ReadonlyArray<ApiItem>;

/** @override */
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
Expand All @@ -92,8 +107,15 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(ba
public readonly [_members]: ApiItem[];
public [_membersSorted]: boolean;
public [_membersByContainerKey]: Map<string, ApiItem>;

// For members of this container that extend ApiNameMixin, this stores the list of members with a given name.
// Examples include merged declarations, overloaded functions, etc.
public [_membersByName]: Map<string, ApiItem[]> | undefined;

// For members of this container that do NOT extend ApiNameMixin, this stores the list of members
// that share a common ApiItemKind. Examples include overloaded constructors or index signatures.
public [_membersByKind]: Map<string, ApiItem[]> | undefined; // key is ApiItemKind

/** @override */
public static onDeserializeInto(options: Partial<IApiItemContainerMixinOptions>,
context: DeserializerContext, jsonObject: IApiItemContainerJson): void {
Expand Down Expand Up @@ -142,6 +164,7 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(ba

this[_members].push(member);
this[_membersByName] = undefined; // invalidate the lookup
this[_membersByKind] = undefined; // invalidate the lookup
this[_membersSorted] = false;
this[_membersByContainerKey].set(member.containerKey, member);

Expand All @@ -153,25 +176,55 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(ba
}

public findMembersByName(name: string): ReadonlyArray<ApiItem> {
// Build the lookup on demand
this._ensureMemberMaps();
return this[_membersByName]!.get(name) || [];
}

/** @internal */
public _getMergedSiblingsForMember(memberApiItem: ApiItem): ReadonlyArray<ApiItem> {
this._ensureMemberMaps();
let result: ApiItem[] | undefined;
if (ApiNameMixin.isBaseClassOf(memberApiItem)) {
result = this[_membersByName]!.get(memberApiItem.name);
} else {
result = this[_membersByKind]!.get(memberApiItem.kind);
}
if (!result) {
throw new InternalError('Item was not found in the _membersByName/_membersByKind lookup');
}
return result;
}

/** @internal */
public _ensureMemberMaps(): void {
// Build the _membersByName and _membersByKind tables if they don't already exist
if (this[_membersByName] === undefined) {
const map: Map<string, ApiItem[]> = new Map<string, ApiItem[]>();
const membersByName: Map<string, ApiItem[]> = new Map<string, ApiItem[]>();
const membersByKind: Map<string, ApiItem[]> = new Map<string, ApiItem[]>();

for (const member of this[_members]) {
let map: Map<string, ApiItem[]> | Map<ApiItemKind, ApiItem[]>;
let key: string | ApiItemKind;

if (ApiNameMixin.isBaseClassOf(member)) {
let list: ApiItem[] | undefined = map.get(member.name);
if (list === undefined) {
list = [];
map.set(member.name, list);
}
list.push(member);
map = membersByName;
key = member.name;
} else {
map = membersByKind;
key = member.kind;
}

let list: ApiItem[] | undefined = map.get(key);
if (list === undefined) {
list = [];
map.set(key, list);
}
list.push(member);
}

this[_membersByName] = map;
this[_membersByName] = membersByName;
this[_membersByKind] = membersByKind;
}

return this[_membersByName]!.get(name) || [];
}

/** @override */
Expand Down
30 changes: 30 additions & 0 deletions build-tests/api-documenter-test/etc/api-documenter-test.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,36 @@
}
]
},
{
"kind": "Class",
"canonicalReference": "api-documenter-test!DocClassInterfaceMerge:class",
"docComment": "/**\n * Class that merges with interface\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare class DocClassInterfaceMerge "
}
],
"releaseTag": "Public",
"name": "DocClassInterfaceMerge",
"members": [],
"implementsTokenRanges": []
},
{
"kind": "Interface",
"canonicalReference": "api-documenter-test!DocClassInterfaceMerge:interface",
"docComment": "/**\n * Interface that merges with class\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export interface DocClassInterfaceMerge "
}
],
"releaseTag": "Public",
"name": "DocClassInterfaceMerge",
"members": [],
"extendsTokenRanges": []
},
{
"kind": "Enum",
"canonicalReference": "api-documenter-test!DocEnum:enum",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export class DocClass1 extends DocBaseClass implements IDocInterface1, IDocInter
tableExample(): void;
}

// @public
export class DocClassInterfaceMerge {
}

// @public
export interface DocClassInterfaceMerge {
}

// @public
export enum DocEnum {
One = 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [api-documenter-test](./api-documenter-test.md) &gt; [DocClassInterfaceMerge](./api-documenter-test.docclassinterfacemerge.md)

## DocClassInterfaceMerge interface

Interface that merges with class

<b>Signature:</b>

```typescript
export interface DocClassInterfaceMerge
```
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This project tests various documentation generation scenarios and doc comment sy
| --- | --- |
| [DocBaseClass](./api-documenter-test.docbaseclass.md) | Example base class |
| [DocClass1](./api-documenter-test.docclass1.md) | This is an example class. |
| [DocClassInterfaceMerge](./api-documenter-test.docclassinterfacemerge.md) | Class that merges with interface |
| [Generic](./api-documenter-test.generic.md) | Generic class. |
| [SystemEvent](./api-documenter-test.systemevent.md) | A class used to exposed events. |

Expand All @@ -34,6 +35,7 @@ This project tests various documentation generation scenarios and doc comment sy

| Interface | Description |
| --- | --- |
| [DocClassInterfaceMerge](./api-documenter-test.docclassinterfacemerge.md) | Interface that merges with class |
| [IDocInterface1](./api-documenter-test.idocinterface1.md) | |
| [IDocInterface2](./api-documenter-test.idocinterface2.md) | |
| [IDocInterface3](./api-documenter-test.idocinterface3.md) | Some less common TypeScript declaration kinds. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ items:
children:
- 'api-documenter-test!DocBaseClass:class'
- 'api-documenter-test!DocClass1:class'
- 'api-documenter-test!DocClassInterfaceMerge:class'
- 'api-documenter-test!DocClassInterfaceMerge:interface'
- 'api-documenter-test!DocEnum:enum'
- 'api-documenter-test!Generic:class'
- 'api-documenter-test!globalFunction:function(1)'
Expand Down Expand Up @@ -78,6 +80,10 @@ references:
name: DocBaseClass
- uid: 'api-documenter-test!DocClass1:class'
name: DocClass1
- uid: 'api-documenter-test!DocClassInterfaceMerge:class'
name: DocClassInterfaceMerge
- uid: 'api-documenter-test!DocClassInterfaceMerge:interface'
name: DocClassInterfaceMerge
- uid: 'api-documenter-test!DocEnum:enum'
name: DocEnum
- uid: 'api-documenter-test!Generic:class'
Expand Down
Loading