From d805ac1cb62c45f9799376f83c6034c5fac07024 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sat, 8 Jun 2019 17:44:50 -0700 Subject: [PATCH 1/4] Add support for emitting deeply linked types --- src/documenters/YamlDocumenter.ts | 328 +++++++++++++++++++++++------- src/yaml/IYamlApiFile.ts | 11 + src/yaml/typescript.schema.json | 24 +++ 3 files changed, 290 insertions(+), 73 deletions(-) diff --git a/src/documenters/YamlDocumenter.ts b/src/documenters/YamlDocumenter.ts index 6b5844f38bb..b91183526e8 100644 --- a/src/documenters/YamlDocumenter.ts +++ b/src/documenters/YamlDocumenter.ts @@ -33,14 +33,23 @@ import { ApiConstructor, ApiFunction, ApiReturnTypeMixin, - ApiTypeParameterListMixin + ApiTypeParameterListMixin, + Excerpt, + ExcerptToken, + ExcerptTokenKind } from '@microsoft/api-extractor-model'; - +import { + DeclarationReference, + Navigation, + Meaning +} from '@microsoft/tsdoc/lib/beta/DeclarationReference'; import { IYamlApiFile, IYamlItem, IYamlSyntax, - IYamlParameter + IYamlParameter, + IYamlReference, + IYamlReferenceSpec } from '../yaml/IYamlApiFile'; import { IYamlTocFile, @@ -64,6 +73,9 @@ export class YamlDocumenter { // then it is excluded from the mapping. Also excluded are ApiItem objects (such as package // and function) which are not typically used as a data type. private _apiItemsByTypeName: Map; + private _knownTypeParameters: Set | undefined; + private _yamlReferences: IYamlReference[] | undefined; + private _uidTypeReferenceCounters: Map; private _outputFolder: string; @@ -71,6 +83,7 @@ export class YamlDocumenter { this._apiModel = apiModel; this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); this._apiItemsByTypeName = new Map(); + this._uidTypeReferenceCounters = new Map(); this._initApiItemsByTypeName(); } @@ -105,67 +118,94 @@ export class YamlDocumenter { } private _visitApiItems(apiItem: ApiDocumentedItem, parentYamlFile: IYamlApiFile | undefined): boolean { - const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); - if (!yamlItem) { - return false; - } - - this.onCustomizeYamlItem(yamlItem); + const savedKnownTypeParameters: Set | undefined = this._knownTypeParameters; + try { + // Track type parameters declared by a declaration so that we do not resolve them + // when looking up types in _linkToUidIfPossible() + if (ApiTypeParameterListMixin.isBaseClassOf(apiItem)) { + this._knownTypeParameters = savedKnownTypeParameters + ? new Set(savedKnownTypeParameters) + : new Set(); + for (const typeParameter of apiItem.typeParameters) { + this._knownTypeParameters.add(typeParameter.name); + } + } - if (this._shouldEmbed(apiItem.kind)) { - if (!parentYamlFile) { - throw new InternalError('Missing file context'); + const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); + if (!yamlItem) { + return false; } - parentYamlFile.items.push(yamlItem); - } else { - const newYamlFile: IYamlApiFile = { - items: [] - }; - newYamlFile.items.push(yamlItem); - let children: ReadonlyArray; - if (apiItem.kind === ApiItemKind.Package) { - // Skip over the entry point, since it's not part of the documentation hierarchy - children = apiItem.members[0].members; + this.onCustomizeYamlItem(yamlItem); + + if (this._shouldEmbed(apiItem.kind)) { + if (!parentYamlFile) { + throw new InternalError('Missing file context'); + } + parentYamlFile.items.push(yamlItem); } else { - children = apiItem.members; - } + const newYamlFile: IYamlApiFile = { + items: [] + }; + newYamlFile.items.push(yamlItem); + + let children: ReadonlyArray; + if (apiItem.kind === ApiItemKind.Package) { + // Skip over the entry point, since it's not part of the documentation hierarchy + children = apiItem.members[0].members; + } else { + children = apiItem.members; + } - const flattenedChildren: ApiItem[] = this._flattenNamespaces(children); + const flattenedChildren: ApiItem[] = this._flattenNamespaces(children); - for (const child of flattenedChildren) { - if (child instanceof ApiDocumentedItem) { - if (this._visitApiItems(child, newYamlFile)) { - if (!yamlItem.children) { - yamlItem.children = []; + for (const child of flattenedChildren) { + if (child instanceof ApiDocumentedItem) { + if (this._visitApiItems(child, newYamlFile)) { + if (!yamlItem.children) { + yamlItem.children = []; + } + yamlItem.children.push(this._getUid(child)); } - yamlItem.children.push(this._getUid(child)); } } - } - - const yamlFilePath: string = this._getYamlFilePath(apiItem); - if (apiItem.kind === ApiItemKind.Package) { - console.log('Writing ' + yamlFilePath); - } + if (this._yamlReferences) { + if (this._yamlReferences.length > 0) { + if (newYamlFile.references) { + newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences]; + } else { + newYamlFile.references = this._yamlReferences; + } + } + this._yamlReferences = undefined; + } - this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); + const yamlFilePath: string = this._getYamlFilePath(apiItem); - if (parentYamlFile) { - if (!parentYamlFile.references) { - parentYamlFile.references = []; + if (apiItem.kind === ApiItemKind.Package) { + console.log('Writing ' + yamlFilePath); } - parentYamlFile.references.push({ - uid: this._getUid(apiItem), - name: this._getYamlItemName(apiItem) - }); + this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); + + if (parentYamlFile) { + if (!parentYamlFile.references) { + parentYamlFile.references = []; + } + parentYamlFile.references.push({ + uid: this._getUid(apiItem), + name: this._getYamlItemName(apiItem) + }); + + } } - } - return true; + return true; + } finally { + this._knownTypeParameters = savedKnownTypeParameters; + } } // Since the YAML schema does not yet support nested namespaces, we simply omit them from @@ -275,8 +315,10 @@ export class YamlDocumenter { return undefined; } - const yamlItem: Partial = { }; - yamlItem.uid = this._getUid(apiItem); + const uid: DeclarationReference = this._getUidObject(apiItem); + const yamlItem: Partial = { + uid: uid.toString() + }; if (apiItem.tsdocComment) { const tsdocComment: DocComment = apiItem.tsdocComment; @@ -328,21 +370,21 @@ export class YamlDocumenter { break; case ApiItemKind.Class: yamlItem.type = 'class'; - this._populateYamlClassOrInterface(yamlItem, apiItem as ApiClass); + this._populateYamlClassOrInterface(uid, yamlItem, apiItem as ApiClass); break; case ApiItemKind.Interface: yamlItem.type = 'interface'; - this._populateYamlClassOrInterface(yamlItem, apiItem as ApiInterface); + this._populateYamlClassOrInterface(uid, yamlItem, apiItem as ApiInterface); break; case ApiItemKind.Method: case ApiItemKind.MethodSignature: yamlItem.type = 'method'; - this._populateYamlFunctionLike(yamlItem, apiItem as ApiMethod | ApiMethodSignature); + this._populateYamlFunctionLike(uid, yamlItem, apiItem as ApiMethod | ApiMethodSignature); break; case ApiItemKind.Constructor: yamlItem.type = 'constructor'; - this._populateYamlFunctionLike(yamlItem, apiItem as ApiConstructor); + this._populateYamlFunctionLike(uid, yamlItem, apiItem as ApiConstructor); break; case ApiItemKind.Package: @@ -356,12 +398,12 @@ export class YamlDocumenter { } else { yamlItem.type = 'property'; } - this._populateYamlProperty(yamlItem, apiProperty); + this._populateYamlProperty(uid, yamlItem, apiProperty); break; case ApiItemKind.Function: yamlItem.type = 'function'; - this._populateYamlFunctionLike(yamlItem, apiItem as ApiFunction); + this._populateYamlFunctionLike(uid, yamlItem, apiItem as ApiFunction); break; default: @@ -379,7 +421,9 @@ export class YamlDocumenter { return yamlItem as IYamlItem; } - private _populateYamlTypeParameters(apiItem: ApiTypeParameterListMixin): IYamlParameter[] { + private _populateYamlTypeParameters(contextUid: DeclarationReference, apiItem: ApiTypeParameterListMixin): + IYamlParameter[] { + const typeParameters: IYamlParameter[] = []; for (const apiTypeParameter of apiItem.typeParameters) { const typeParameter: IYamlParameter = { @@ -391,7 +435,7 @@ export class YamlDocumenter { } if (!apiTypeParameter.constraintExcerpt.isEmpty) { - typeParameter.type = [ this._linkToUidIfPossible(apiTypeParameter.constraintExcerpt.text) ]; + typeParameter.type = [ this._renderType(contextUid, apiTypeParameter.constraintExcerpt) ]; } typeParameters.push(typeParameter); @@ -399,26 +443,28 @@ export class YamlDocumenter { return typeParameters; } - private _populateYamlClassOrInterface(yamlItem: Partial, apiItem: ApiClass | ApiInterface): void { + private _populateYamlClassOrInterface(uid: DeclarationReference, yamlItem: Partial, apiItem: ApiClass | + ApiInterface): void { + if (apiItem instanceof ApiClass) { if (apiItem.extendsType) { - yamlItem.extends = [ this._linkToUidIfPossible(apiItem.extendsType.excerpt.text) ]; + yamlItem.extends = [ this._renderType(uid, apiItem.extendsType.excerpt) ]; } if (apiItem.implementsTypes.length > 0) { yamlItem.implements = []; for (const implementsType of apiItem.implementsTypes) { - yamlItem.implements.push(this._linkToUidIfPossible(implementsType.excerpt.text)); + yamlItem.implements.push(this._renderType(uid, implementsType.excerpt)); } } } else if (apiItem instanceof ApiInterface) { if (apiItem.extendsTypes.length > 0) { yamlItem.extends = []; for (const extendsType of apiItem.extendsTypes) { - yamlItem.extends.push(this._linkToUidIfPossible(extendsType.excerpt.text)); + yamlItem.extends.push(this._renderType(uid, extendsType.excerpt)); } } - const typeParameters: IYamlParameter[] = this._populateYamlTypeParameters(apiItem); + const typeParameters: IYamlParameter[] = this._populateYamlTypeParameters(uid, apiItem); if (typeParameters.length) { yamlItem.syntax = { typeParameters }; } @@ -441,8 +487,8 @@ export class YamlDocumenter { } } - private _populateYamlFunctionLike(yamlItem: Partial, apiItem: ApiMethod | ApiMethodSignature - | ApiConstructor | ApiFunction): void { + private _populateYamlFunctionLike(uid: DeclarationReference, yamlItem: Partial, apiItem: ApiMethod | + ApiMethodSignature | ApiConstructor | ApiFunction): void { const syntax: IYamlSyntax = { content: apiItem.getExcerptWithModifiers() @@ -450,7 +496,7 @@ export class YamlDocumenter { yamlItem.syntax = syntax; if (ApiReturnTypeMixin.isBaseClassOf(apiItem)) { - const returnType: string = this._linkToUidIfPossible(apiItem.returnTypeExcerpt.text); + const returnType: string = this._renderType(uid, apiItem.returnTypeExcerpt); let returnDescription: string = ''; @@ -479,7 +525,7 @@ export class YamlDocumenter { { id: apiParameter.name, description: parameterDescription, - type: [ this._linkToUidIfPossible(apiParameter.parameterTypeExcerpt.text) ] + type: [ this._renderType(uid, apiParameter.parameterTypeExcerpt) ] } as IYamlParameter ); } @@ -489,7 +535,7 @@ export class YamlDocumenter { } if (ApiTypeParameterListMixin.isBaseClassOf(apiItem)) { - const typeParameters: IYamlParameter[] = this._populateYamlTypeParameters(apiItem); + const typeParameters: IYamlParameter[] = this._populateYamlTypeParameters(uid, apiItem); if (typeParameters.length) { syntax.typeParameters = typeParameters; } @@ -497,7 +543,9 @@ export class YamlDocumenter { } - private _populateYamlProperty(yamlItem: Partial, apiItem: ApiPropertyItem): void { + private _populateYamlProperty(uid: DeclarationReference, yamlItem: Partial, apiItem: ApiPropertyItem): + void { + const syntax: IYamlSyntax = { content: apiItem.getExcerptWithModifiers() }; @@ -505,7 +553,7 @@ export class YamlDocumenter { if (apiItem.propertyTypeExcerpt.text) { syntax.return = { - type: [ this._linkToUidIfPossible(apiItem.propertyTypeExcerpt.text) ] + type: [ this._renderType(uid, apiItem.propertyTypeExcerpt) ] }; } } @@ -553,7 +601,11 @@ export class YamlDocumenter { * Example: `node-core-library!JsonFile#load` */ protected _getUid(apiItem: ApiItem): string { - return apiItem.canonicalReference.toString(); + return this._getUidObject(apiItem).toString(); + } + + protected _getUidObject(apiItem: ApiItem): DeclarationReference { + return apiItem.canonicalReference; } /** @@ -615,6 +667,13 @@ export class YamlDocumenter { } } + private _ensureYamlReferences(): IYamlReference[] { + if (!this._yamlReferences) { + this._yamlReferences = []; + } + return this._yamlReferences; + } + /** * This is a temporary workaround to enable limited autolinking of API item types * until the YAML file format is enhanced to support general hyperlinks. @@ -625,14 +684,131 @@ export class YamlDocumenter { * contain any Markdown links. The _substituteUidForSimpleType() function assumes * it is given #2 but substitutes #1 if the name can be matched to a ApiItem. */ - private _linkToUidIfPossible(typeName: string): string { + private _linkToUidIfPossible(typeName: string): string | undefined { + typeName = typeName.trim(); + // Do not look up the UID for a type parameter, as we could inadvertently + // look up a different type with the same name. + if (this._knownTypeParameters && this._knownTypeParameters.has(typeName)) { + return typeName; + } + // Note that typeName might be a _getTypeNameWithDot() name or it might be a simple class name - const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(typeName.trim()); + const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(typeName); if (apiItem) { // Substitute the UID return this._getUid(apiItem); } - return typeName; + } + + private _renderType(contextUid: DeclarationReference, typeExcerpt: Excerpt): string { + const excerptTokens: ExcerptToken[] = typeExcerpt.tokens.slice( + typeExcerpt.tokenRange.startIndex, + typeExcerpt.tokenRange.endIndex); + + if (excerptTokens.length === 0) { + return ''; + } + + // Remove the last token if it consists only of whitespace + const lastToken: ExcerptToken = excerptTokens[excerptTokens.length - 1]; + if (lastToken.kind === ExcerptTokenKind.Content && !lastToken.text.trim()) { + excerptTokens.pop(); + if (excerptTokens.length === 0) { + return ''; + } + } + + const typeName: string = typeExcerpt.text.trim(); + if (excerptTokens.length === 1 && + excerptTokens[0].kind === ExcerptTokenKind.Reference && + excerptTokens[0].canonicalReference) { + return this._recordYamlReference( + excerptTokens[0].canonicalReference.toString(), + typeName + ); + } + + const typeNameAsUid: string | undefined = this._linkToUidIfPossible(typeName); + if (typeNameAsUid !== undefined) { + if (typeNameAsUid !== typeName) { + return this._recordYamlReference(typeNameAsUid, typeName); + } + return typeNameAsUid; + } + + if (isEntityName(typeName)) { + return typeName; + } + + // If there are no references to be used for a complex type, return the type name. + if (!excerptTokens.some(tok => tok.kind === ExcerptTokenKind.Reference)) { + return typeName; + } + + const baseUid: string = contextUid + .withMeaning(undefined) + .withOverloadIndex(undefined) + .toString(); + const counter: number = this._uidTypeReferenceCounters.get(baseUid) || 0; + const uid: string = contextUid + .addNavigationStep(Navigation.Locals, `${counter}`) + .withMeaning(Meaning.ComplexType) + .toString(); + this._uidTypeReferenceCounters.set(baseUid, counter + 1); + this._recordYamlReference(uid, typeName, excerptTokens); + return uid; + } + + private _recordYamlReference(uid: string, typeName: string, excerptTokens?: ExcerptToken[]): string { + const yamlReferences: IYamlReference[] = this._ensureYamlReferences(); + + if (yamlReferences.some(ref => ref.uid === uid)) { + return uid; + } + + // Fill in the reference spec from the excerpt. + const spec: IYamlReferenceSpec[] = []; + if (excerptTokens) { + for (const token of excerptTokens) { + if (token.kind === ExcerptTokenKind.Reference) { + let specUid: string | undefined = token.canonicalReference && token.canonicalReference.toString(); + const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(token.text); + if (specUid === undefined) { + if (apiItem) { + specUid = this._getUid(apiItem); + } else { + specUid = token.text; + } + } + spec.push( + { + uid: specUid, + name: token.text, + fullName: apiItem ? apiItem.getScopedNameWithinPackage() : token.text + } + ); + } else { + spec.push( + { + name: token.text, + fullName: token.text + } + ); + } + } + } + + const yamlReference: IYamlReference = { uid }; + if (spec.length > 0) { + yamlReference.name = spec.map(s => s.name).join('').trim(); + yamlReference.fullName = spec.map(s => s.fullName || s.name).join('').trim(); + yamlReference['spec.typeScript'] = spec; + } else if (typeName !== uid) { + yamlReference.name = typeName; + } + + yamlReferences.push(yamlReference); + return uid; } /** @@ -721,3 +897,9 @@ export class YamlDocumenter { FileSystem.ensureEmptyFolder(this._outputFolder); } } + +const entityNameRegExp: RegExp = /^(?!\d)[\w$_]+(\.(?!\d)[\w$_]+)*$/; + +function isEntityName(text: string): boolean { + return entityNameRegExp.test(text); +} \ No newline at end of file diff --git a/src/yaml/IYamlApiFile.ts b/src/yaml/IYamlApiFile.ts index 450a565a42e..3eb0d7cf5ee 100644 --- a/src/yaml/IYamlApiFile.ts +++ b/src/yaml/IYamlApiFile.ts @@ -73,8 +73,19 @@ export interface IYamlParameter { * declared in a separate YAML file. */ export interface IYamlReference { + uid?: string; name?: string; + fullName?: string; + 'spec.typeScript'?: IYamlReferenceSpec[]; +} + +/** + * Part of the IYamlApiFile structure. Represents a text specification for a reference. + */ +export interface IYamlReferenceSpec { uid?: string; + name?: string; + fullName?: string; } /** diff --git a/src/yaml/typescript.schema.json b/src/yaml/typescript.schema.json index c67b8f3c813..b10570c3a70 100644 --- a/src/yaml/typescript.schema.json +++ b/src/yaml/typescript.schema.json @@ -159,11 +159,35 @@ "type": "object", "additionalProperties": false, "properties": { + "uid": { + "type": "string" + }, "name": { "type": "string" }, + "fullName": { + "type": "string" + }, + "spec.typeScript": { + "type": "array", + "items": { + "$ref": "#/definitions/spec" + } + } + } + }, + "spec": { + "type": "object", + "additionalProperties": false, + "properties": { "uid": { "type": "string" + }, + "name": { + "type": "string" + }, + "fullName": { + "type": "string" } } }, From 62cc81af817e79ec4f855aa5211b80880c502700 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Fri, 6 Sep 2019 13:09:32 -0700 Subject: [PATCH 2/4] Cleanup _renderType, remove _linkToUidIfPossible and related members --- src/documenters/YamlDocumenter.ts | 228 +++++++++++------------------- 1 file changed, 81 insertions(+), 147 deletions(-) diff --git a/src/documenters/YamlDocumenter.ts b/src/documenters/YamlDocumenter.ts index b91183526e8..c52f732314a 100644 --- a/src/documenters/YamlDocumenter.ts +++ b/src/documenters/YamlDocumenter.ts @@ -60,6 +60,11 @@ import { CustomMarkdownEmitter} from '../markdown/CustomMarkdownEmitter'; const yamlApiSchema: JsonSchema = JsonSchema.fromFile(path.join(__dirname, '..', 'yaml', 'typescript.schema.json')); +interface IYamlReferences { + references: IYamlReference[]; + typeNameToUid: Map; +} + /** * Writes documentation in the Universal Reference YAML file format, as defined by typescript.schema.json. */ @@ -67,14 +72,9 @@ export class YamlDocumenter { private readonly _apiModel: ApiModel; private readonly _markdownEmitter: CustomMarkdownEmitter; - // This is used by the _linkToUidIfPossible() workaround. - // It stores a mapping from type name (e.g. "MyClass") to the corresponding ApiItem. - // If the mapping would be ambiguous (e.g. "MyClass" is defined by multiple packages) - // then it is excluded from the mapping. Also excluded are ApiItem objects (such as package - // and function) which are not typically used as a data type. - private _apiItemsByTypeName: Map; + private _apiItemsByCanonicalReference: Map; private _knownTypeParameters: Set | undefined; - private _yamlReferences: IYamlReference[] | undefined; + private _yamlReferences: IYamlReferences | undefined; private _uidTypeReferenceCounters: Map; private _outputFolder: string; @@ -82,10 +82,10 @@ export class YamlDocumenter { public constructor(apiModel: ApiModel) { this._apiModel = apiModel; this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); - this._apiItemsByTypeName = new Map(); + this._apiItemsByCanonicalReference = new Map(); this._uidTypeReferenceCounters = new Map(); - this._initApiItemsByTypeName(); + this._initApiItems(); } /** @virtual */ @@ -171,11 +171,11 @@ export class YamlDocumenter { } if (this._yamlReferences) { - if (this._yamlReferences.length > 0) { + if (this._yamlReferences.references.length > 0) { if (newYamlFile.references) { - newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences]; + newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences.references]; } else { - newYamlFile.references = this._yamlReferences; + newYamlFile.references = this._yamlReferences.references; } } this._yamlReferences = undefined; @@ -609,97 +609,39 @@ export class YamlDocumenter { } /** - * Initialize the _apiItemsByTypeName data structure. + * Initialize the _apiItemsByCanonicalReference data structure. */ - private _initApiItemsByTypeName(): void { - // Collect the _apiItemsByTypeName table - const ambiguousNames: Set = new Set(); - - this._initApiItemsByTypeNameRecursive(this._apiModel, ambiguousNames); + private _initApiItems(): void { + this._initApiItemsRecursive(this._apiModel); - // Remove the ambiguous matches - for (const ambiguousName of ambiguousNames) { - this._apiItemsByTypeName.delete(ambiguousName); - } } /** - * Helper for _initApiItemsByTypeName() + * Helper for _initApiItems() */ - private _initApiItemsByTypeNameRecursive(apiItem: ApiItem, ambiguousNames: Set): void { - switch (apiItem.kind) { - case ApiItemKind.Class: - case ApiItemKind.Enum: - case ApiItemKind.Interface: - // Attempt to register both the fully qualified name and the short name - const namesForType: string[] = [apiItem.displayName]; - - // Note that nameWithDot cannot conflict with apiItem.name (because apiItem.name - // cannot contain a dot) - const nameWithDot: string | undefined = this._getTypeNameWithDot(apiItem); - if (nameWithDot) { - namesForType.push(nameWithDot); - } - - // Register all names - for (const typeName of namesForType) { - if (ambiguousNames.has(typeName)) { - break; - } - - if (this._apiItemsByTypeName.has(typeName)) { - // We saw this name before, so it's an ambiguous match - ambiguousNames.add(typeName); - break; - } - - this._apiItemsByTypeName.set(typeName, apiItem); - } - - break; + private _initApiItemsRecursive(apiItem: ApiItem): void { + if (apiItem.canonicalReference && !apiItem.canonicalReference.isEmpty) { + this._apiItemsByCanonicalReference.set(apiItem.canonicalReference.toString(), apiItem); } // Recurse container members if (ApiItemContainerMixin.isBaseClassOf(apiItem)) { for (const apiMember of apiItem.members) { - this._initApiItemsByTypeNameRecursive(apiMember, ambiguousNames); + this._initApiItemsRecursive(apiMember); } } } - private _ensureYamlReferences(): IYamlReference[] { + private _ensureYamlReferences(): IYamlReferences { if (!this._yamlReferences) { - this._yamlReferences = []; + this._yamlReferences = { + references: [], + typeNameToUid: new Map() + }; } return this._yamlReferences; } - /** - * This is a temporary workaround to enable limited autolinking of API item types - * until the YAML file format is enhanced to support general hyperlinks. - * @remarks - * In the current version, fields such as IApiProperty.type allow either: - * (1) a UID identifier such as "node-core-library.JsonFile" which will be rendered - * as a hyperlink to that type name, or (2) a block of freeform text that must not - * contain any Markdown links. The _substituteUidForSimpleType() function assumes - * it is given #2 but substitutes #1 if the name can be matched to a ApiItem. - */ - private _linkToUidIfPossible(typeName: string): string | undefined { - typeName = typeName.trim(); - // Do not look up the UID for a type parameter, as we could inadvertently - // look up a different type with the same name. - if (this._knownTypeParameters && this._knownTypeParameters.has(typeName)) { - return typeName; - } - - // Note that typeName might be a _getTypeNameWithDot() name or it might be a simple class name - const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(typeName); - if (apiItem) { - // Substitute the UID - return this._getUid(apiItem); - } - } - private _renderType(contextUid: DeclarationReference, typeExcerpt: Excerpt): string { const excerptTokens: ExcerptToken[] = typeExcerpt.tokens.slice( typeExcerpt.tokenRange.startIndex, @@ -719,76 +661,87 @@ export class YamlDocumenter { } const typeName: string = typeExcerpt.text.trim(); + + // Record a reference to a type parameter as its name, so as not to resolve to a conflicting name + if (this._knownTypeParameters && this._knownTypeParameters.has(typeName)) { + return this._recordYamlReference(this._ensureYamlReferences(), typeName, typeName); + } + + // If there are no references to be used for a complex type, return the type name. + if (!excerptTokens.some(tok => tok.kind === ExcerptTokenKind.Reference && !!tok.canonicalReference)) { + return typeName; + } + + const yamlReferences: IYamlReferences = this._ensureYamlReferences(); + const existingUid: string | undefined = yamlReferences.typeNameToUid.get(typeName); + + // If this type has already been referenced for the current file, return its uid. + if (existingUid) { + return existingUid; + } + + // If the excerpt consists of a single reference token, record the reference. if (excerptTokens.length === 1 && excerptTokens[0].kind === ExcerptTokenKind.Reference && excerptTokens[0].canonicalReference) { return this._recordYamlReference( + yamlReferences, excerptTokens[0].canonicalReference.toString(), typeName ); } - const typeNameAsUid: string | undefined = this._linkToUidIfPossible(typeName); - if (typeNameAsUid !== undefined) { - if (typeNameAsUid !== typeName) { - return this._recordYamlReference(typeNameAsUid, typeName); - } - return typeNameAsUid; - } - - if (isEntityName(typeName)) { - return typeName; - } - - // If there are no references to be used for a complex type, return the type name. - if (!excerptTokens.some(tok => tok.kind === ExcerptTokenKind.Reference)) { - return typeName; - } - + // Otherwise, the type is complex and consists of one or more reference tokens. Record a reference + // and return its uid. const baseUid: string = contextUid .withMeaning(undefined) .withOverloadIndex(undefined) .toString(); + + // Keep track of the count for the base uid (without meaning or overload index) to ensure + // that each complex type reference is unique. const counter: number = this._uidTypeReferenceCounters.get(baseUid) || 0; + this._uidTypeReferenceCounters.set(baseUid, counter + 1); + const uid: string = contextUid .addNavigationStep(Navigation.Locals, `${counter}`) .withMeaning(Meaning.ComplexType) + .withOverloadIndex(undefined) .toString(); - this._uidTypeReferenceCounters.set(baseUid, counter + 1); - this._recordYamlReference(uid, typeName, excerptTokens); - return uid; + + return this._recordYamlReference(yamlReferences, uid, typeName, excerptTokens); } - private _recordYamlReference(uid: string, typeName: string, excerptTokens?: ExcerptToken[]): string { - const yamlReferences: IYamlReference[] = this._ensureYamlReferences(); + private _recordYamlReference(yamlReferences: IYamlReferences, uid: string, typeName: string, + excerptTokens?: ExcerptToken[]): string { - if (yamlReferences.some(ref => ref.uid === uid)) { + if (yamlReferences.references.some(ref => ref.uid === uid)) { return uid; } // Fill in the reference spec from the excerpt. - const spec: IYamlReferenceSpec[] = []; + const specs: IYamlReferenceSpec[] = []; if (excerptTokens) { for (const token of excerptTokens) { if (token.kind === ExcerptTokenKind.Reference) { - let specUid: string | undefined = token.canonicalReference && token.canonicalReference.toString(); - const apiItem: ApiItem | undefined = this._apiItemsByTypeName.get(token.text); - if (specUid === undefined) { - if (apiItem) { - specUid = this._getUid(apiItem); - } else { - specUid = token.text; - } + const spec: IYamlReferenceSpec = { }; + const specUid: string | undefined = token.canonicalReference && token.canonicalReference.toString(); + const apiItem: ApiItem | undefined = specUid ? this._apiItemsByCanonicalReference.get(specUid) : undefined; + if (specUid) { + spec.uid = specUid; } - spec.push( - { - uid: specUid, - name: token.text, - fullName: apiItem ? apiItem.getScopedNameWithinPackage() : token.text - } - ); + spec.name = token.text; + spec.fullName = + apiItem ? apiItem.getScopedNameWithinPackage() : + token.canonicalReference ? token.canonicalReference + .withSource(undefined) + .withMeaning(undefined) + .withOverloadIndex(undefined) + .toString() : + token.text; + specs.push(spec); } else { - spec.push( + specs.push( { name: token.text, fullName: token.text @@ -799,31 +752,18 @@ export class YamlDocumenter { } const yamlReference: IYamlReference = { uid }; - if (spec.length > 0) { - yamlReference.name = spec.map(s => s.name).join('').trim(); - yamlReference.fullName = spec.map(s => s.fullName || s.name).join('').trim(); - yamlReference['spec.typeScript'] = spec; + if (specs.length > 0) { + yamlReference.name = specs.map(s => s.name).join('').trim(); + yamlReference.fullName = specs.map(s => s.fullName || s.name).join('').trim(); + yamlReference['spec.typeScript'] = specs; } else if (typeName !== uid) { yamlReference.name = typeName; } - yamlReferences.push(yamlReference); + yamlReferences.references.push(yamlReference); return uid; } - /** - * If the apiItem represents a scoped name such as "my-library#MyNamespace.MyClass", - * this returns a string such as "MyNamespace.MyClass". If the result would not - * have at least one dot in it, then undefined is returned. - */ - private _getTypeNameWithDot(apiItem: ApiItem): string | undefined { - const result: string = apiItem.getScopedNameWithinPackage(); - if (result.indexOf('.') >= 0) { - return result; - } - return undefined; - } - private _getYamlItemName(apiItem: ApiItem): string { if (apiItem.parent && apiItem.parent.kind === ApiItemKind.Namespace) { // If the immediate parent is a namespace, then add the namespaces to the name. For example: @@ -896,10 +836,4 @@ export class YamlDocumenter { console.log('Deleting old output from ' + this._outputFolder); FileSystem.ensureEmptyFolder(this._outputFolder); } -} - -const entityNameRegExp: RegExp = /^(?!\d)[\w$_]+(\.(?!\d)[\w$_]+)*$/; - -function isEntityName(text: string): boolean { - return entityNameRegExp.test(text); } \ No newline at end of file From 6310d73c14d94fbadce87458295c6ea5d5f0857f Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sat, 7 Sep 2019 15:16:53 -0700 Subject: [PATCH 3/4] Remove yaml references for type params and toJSON/fromJSON from ExcerptToken --- src/documenters/YamlDocumenter.ts | 136 +++++++++++++----------------- 1 file changed, 57 insertions(+), 79 deletions(-) diff --git a/src/documenters/YamlDocumenter.ts b/src/documenters/YamlDocumenter.ts index c52f732314a..0826198fa1b 100644 --- a/src/documenters/YamlDocumenter.ts +++ b/src/documenters/YamlDocumenter.ts @@ -63,6 +63,7 @@ const yamlApiSchema: JsonSchema = JsonSchema.fromFile(path.join(__dirname, '..', interface IYamlReferences { references: IYamlReference[]; typeNameToUid: Map; + uidTypeReferenceCounters: Map; } /** @@ -73,9 +74,7 @@ export class YamlDocumenter { private readonly _markdownEmitter: CustomMarkdownEmitter; private _apiItemsByCanonicalReference: Map; - private _knownTypeParameters: Set | undefined; private _yamlReferences: IYamlReferences | undefined; - private _uidTypeReferenceCounters: Map; private _outputFolder: string; @@ -83,7 +82,6 @@ export class YamlDocumenter { this._apiModel = apiModel; this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); this._apiItemsByCanonicalReference = new Map(); - this._uidTypeReferenceCounters = new Map(); this._initApiItems(); } @@ -118,94 +116,78 @@ export class YamlDocumenter { } private _visitApiItems(apiItem: ApiDocumentedItem, parentYamlFile: IYamlApiFile | undefined): boolean { - const savedKnownTypeParameters: Set | undefined = this._knownTypeParameters; - try { - // Track type parameters declared by a declaration so that we do not resolve them - // when looking up types in _linkToUidIfPossible() - if (ApiTypeParameterListMixin.isBaseClassOf(apiItem)) { - this._knownTypeParameters = savedKnownTypeParameters - ? new Set(savedKnownTypeParameters) - : new Set(); - for (const typeParameter of apiItem.typeParameters) { - this._knownTypeParameters.add(typeParameter.name); - } - } + const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); + if (!yamlItem) { + return false; + } - const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); - if (!yamlItem) { - return false; - } + this.onCustomizeYamlItem(yamlItem); - this.onCustomizeYamlItem(yamlItem); + if (this._shouldEmbed(apiItem.kind)) { + if (!parentYamlFile) { + throw new InternalError('Missing file context'); + } + parentYamlFile.items.push(yamlItem); + } else { + const newYamlFile: IYamlApiFile = { + items: [] + }; + newYamlFile.items.push(yamlItem); - if (this._shouldEmbed(apiItem.kind)) { - if (!parentYamlFile) { - throw new InternalError('Missing file context'); - } - parentYamlFile.items.push(yamlItem); + let children: ReadonlyArray; + if (apiItem.kind === ApiItemKind.Package) { + // Skip over the entry point, since it's not part of the documentation hierarchy + children = apiItem.members[0].members; } else { - const newYamlFile: IYamlApiFile = { - items: [] - }; - newYamlFile.items.push(yamlItem); - - let children: ReadonlyArray; - if (apiItem.kind === ApiItemKind.Package) { - // Skip over the entry point, since it's not part of the documentation hierarchy - children = apiItem.members[0].members; - } else { - children = apiItem.members; - } + children = apiItem.members; + } - const flattenedChildren: ApiItem[] = this._flattenNamespaces(children); + const flattenedChildren: ApiItem[] = this._flattenNamespaces(children); - for (const child of flattenedChildren) { - if (child instanceof ApiDocumentedItem) { - if (this._visitApiItems(child, newYamlFile)) { - if (!yamlItem.children) { - yamlItem.children = []; - } - yamlItem.children.push(this._getUid(child)); + for (const child of flattenedChildren) { + if (child instanceof ApiDocumentedItem) { + if (this._visitApiItems(child, newYamlFile)) { + if (!yamlItem.children) { + yamlItem.children = []; } + yamlItem.children.push(this._getUid(child)); } } + } - if (this._yamlReferences) { - if (this._yamlReferences.references.length > 0) { - if (newYamlFile.references) { - newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences.references]; - } else { - newYamlFile.references = this._yamlReferences.references; - } + if (this._yamlReferences) { + if (this._yamlReferences.references.length > 0) { + if (newYamlFile.references) { + newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences.references]; + } else { + newYamlFile.references = this._yamlReferences.references; } - this._yamlReferences = undefined; } + this._yamlReferences = undefined; + } - const yamlFilePath: string = this._getYamlFilePath(apiItem); + const yamlFilePath: string = this._getYamlFilePath(apiItem); - if (apiItem.kind === ApiItemKind.Package) { - console.log('Writing ' + yamlFilePath); - } + if (apiItem.kind === ApiItemKind.Package) { + console.log('Writing ' + yamlFilePath); + } - this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); + this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); - if (parentYamlFile) { - if (!parentYamlFile.references) { - parentYamlFile.references = []; - } + if (parentYamlFile) { + if (!parentYamlFile.references) { + parentYamlFile.references = []; + } - parentYamlFile.references.push({ - uid: this._getUid(apiItem), - name: this._getYamlItemName(apiItem) - }); + parentYamlFile.references.push({ + uid: this._getUid(apiItem), + name: this._getYamlItemName(apiItem) + }); - } } - - return true; - } finally { - this._knownTypeParameters = savedKnownTypeParameters; } + + return true; } // Since the YAML schema does not yet support nested namespaces, we simply omit them from @@ -636,7 +618,8 @@ export class YamlDocumenter { if (!this._yamlReferences) { this._yamlReferences = { references: [], - typeNameToUid: new Map() + typeNameToUid: new Map(), + uidTypeReferenceCounters: new Map() }; } return this._yamlReferences; @@ -662,11 +645,6 @@ export class YamlDocumenter { const typeName: string = typeExcerpt.text.trim(); - // Record a reference to a type parameter as its name, so as not to resolve to a conflicting name - if (this._knownTypeParameters && this._knownTypeParameters.has(typeName)) { - return this._recordYamlReference(this._ensureYamlReferences(), typeName, typeName); - } - // If there are no references to be used for a complex type, return the type name. if (!excerptTokens.some(tok => tok.kind === ExcerptTokenKind.Reference && !!tok.canonicalReference)) { return typeName; @@ -700,8 +678,8 @@ export class YamlDocumenter { // Keep track of the count for the base uid (without meaning or overload index) to ensure // that each complex type reference is unique. - const counter: number = this._uidTypeReferenceCounters.get(baseUid) || 0; - this._uidTypeReferenceCounters.set(baseUid, counter + 1); + const counter: number = yamlReferences.uidTypeReferenceCounters.get(baseUid) || 0; + yamlReferences.uidTypeReferenceCounters.set(baseUid, counter + 1); const uid: string = contextUid .addNavigationStep(Navigation.Locals, `${counter}`) From 32c955d9c3c9076954c8cfa3d2f4158184046884 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 10 Sep 2019 11:03:37 -0700 Subject: [PATCH 4/4] Fix issue with duplicate yaml references --- src/documenters/YamlDocumenter.ts | 32 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/documenters/YamlDocumenter.ts b/src/documenters/YamlDocumenter.ts index 0826198fa1b..be33d59dd6a 100644 --- a/src/documenters/YamlDocumenter.ts +++ b/src/documenters/YamlDocumenter.ts @@ -116,6 +116,12 @@ export class YamlDocumenter { } private _visitApiItems(apiItem: ApiDocumentedItem, parentYamlFile: IYamlApiFile | undefined): boolean { + let savedYamlReferences: IYamlReferences | undefined; + if (!this._shouldEmbed(apiItem.kind)) { + savedYamlReferences = this._yamlReferences; + this._yamlReferences = undefined; + } + const yamlItem: IYamlItem | undefined = this._generateYamlItem(apiItem); if (!yamlItem) { return false; @@ -155,17 +161,12 @@ export class YamlDocumenter { } } - if (this._yamlReferences) { - if (this._yamlReferences.references.length > 0) { - if (newYamlFile.references) { - newYamlFile.references = [...newYamlFile.references, ...this._yamlReferences.references]; - } else { - newYamlFile.references = this._yamlReferences.references; - } - } - this._yamlReferences = undefined; + if (this._yamlReferences && this._yamlReferences.references.length > 0) { + newYamlFile.references = this._yamlReferences.references; } + this._yamlReferences = savedYamlReferences; + const yamlFilePath: string = this._getYamlFilePath(apiItem); if (apiItem.kind === ApiItemKind.Package) { @@ -175,15 +176,10 @@ export class YamlDocumenter { this._writeYamlFile(newYamlFile, yamlFilePath, 'UniversalReference', yamlApiSchema); if (parentYamlFile) { - if (!parentYamlFile.references) { - parentYamlFile.references = []; - } - - parentYamlFile.references.push({ - uid: this._getUid(apiItem), - name: this._getYamlItemName(apiItem) - }); - + this._recordYamlReference( + this._ensureYamlReferences(), + this._getUid(apiItem), + this._getYamlItemName(apiItem)); } }