UNPKG

@microsoft/api-extractor

Version:

Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups

855 lines • 43.6 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. /* eslint-disable no-bitwise */ import * as path from 'node:path'; import * as ts from 'typescript'; import { ApiModel, ApiClass, ApiPackage, ApiEntryPoint, ApiMethod, ApiNamespace, ApiInterface, ApiPropertySignature, ReleaseTag, ApiProperty, ApiMethodSignature, ApiEnum, ApiEnumMember, ApiConstructor, ApiConstructSignature, ApiFunction, ApiIndexSignature, ApiVariable, ApiTypeAlias, ApiCallSignature, EnumMemberOrder } from '@microsoft/api-extractor-model'; import { Path } from '@rushstack/node-core-library'; import { ExcerptBuilder } from './ExcerptBuilder'; import { AstSymbol } from '../analyzer/AstSymbol'; import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; import { DtsEmitHelpers } from './DtsEmitHelpers'; export class ApiModelGenerator { #collector; #apiModel; #referenceGenerator; #releaseTagsToTrim; docModelEnabled; constructor(collector, extractorConfig) { this.#collector = collector; this.#apiModel = new ApiModel(); this.#referenceGenerator = new DeclarationReferenceGenerator(collector); const apiModelGenerationOptions = extractorConfig.docModelGenerationOptions; if (apiModelGenerationOptions) { this.#releaseTagsToTrim = apiModelGenerationOptions.releaseTagsToTrim; this.docModelEnabled = true; } else { this.docModelEnabled = false; } } get apiModel() { return this.#apiModel; } buildApiPackage() { const packageDocComment = this.#collector.workingPackage.tsdocComment; const apiPackage = new ApiPackage({ name: this.#collector.workingPackage.name, docComment: packageDocComment, tsdocConfiguration: this.#collector.extractorConfig.tsdocConfiguration, projectFolderUrl: this.#collector.extractorConfig.projectFolderUrl }); this.#apiModel.addMember(apiPackage); const apiEntryPoint = new ApiEntryPoint({ name: '' }); apiPackage.addMember(apiEntryPoint); for (const entity of this.#collector.entities) { // Only process entities that are exported from the entry point. Entities that are exported from // `AstNamespaceImport` entities will be processed by `_processAstNamespaceImport`. However, if // we are including forgotten exports, then process everything. if (entity.exportedFromEntryPoint || this.#collector.extractorConfig.docModelIncludeForgottenExports) { this.#processAstEntity(entity.astEntity, { name: entity.nameForEmit, isExported: entity.exportedFromEntryPoint, parentApiItem: apiEntryPoint }); } } return apiPackage; } #processAstEntity(astEntity, context) { if (astEntity instanceof AstSymbol) { // Skip ancillary declarations; we will process them with the main declaration for (const astDeclaration of this.#collector.getNonAncillaryDeclarations(astEntity)) { this.#processDeclaration(astDeclaration, context); } return; } if (astEntity instanceof AstNamespaceImport) { // Note that a single API item can belong to two different AstNamespaceImport namespaces. For example: // // // file.ts defines "thing()" // import * as example1 from "./file"; // import * as example2 from "./file"; // // // ...so here we end up with example1.thing() and example2.thing() // export { example1, example2 } // // The current logic does not try to associate "thing()" with a specific parent. Instead // the API documentation will show duplicated entries for example1.thing() and example2.thing(). // // This could be improved in the future, but it requires a stable mechanism for choosing an associated parent. // For thoughts about this: https://github.com/microsoft/rushstack/issues/1308 this.#processAstNamespaceImport(astEntity, context); return; } // TODO: Figure out how to represent reexported AstImport objects. Basically we need to introduce a new // ApiItem subclass for "export alias", similar to a type alias, but representing declarations of the // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. } #processAstNamespaceImport(astNamespaceImport, context) { const astModule = astNamespaceImport.astModule; const { name, isExported, parentApiItem } = context; const containerKey = ApiNamespace.getContainerKey(name); const fileUrlPath = this.#getFileUrlPath(astNamespaceImport.declaration); let apiNamespace = parentApiItem.tryGetMemberByKey(containerKey); if (apiNamespace === undefined) { apiNamespace = new ApiNamespace({ name, docComment: undefined, releaseTag: ReleaseTag.None, excerptTokens: [], isExported, fileUrlPath }); parentApiItem.addMember(apiNamespace); } astModule.astModuleExportInfo.exportedLocalEntities.forEach((exportedEntity, exportedName) => { this.#processAstEntity(exportedEntity, { name: exportedName, isExported: true, parentApiItem: apiNamespace }); }); } #processDeclaration(astDeclaration, context) { if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { return; // trim out private declarations } const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const releaseTag = apiItemMetadata.effectiveReleaseTag; if (this.#releaseTagsToTrim?.has(releaseTag)) { return; } switch (astDeclaration.declaration.kind) { case ts.SyntaxKind.CallSignature: this.#processApiCallSignature(astDeclaration, context); break; case ts.SyntaxKind.Constructor: this.#processApiConstructor(astDeclaration, context); break; case ts.SyntaxKind.ConstructSignature: this.#processApiConstructSignature(astDeclaration, context); break; case ts.SyntaxKind.ClassDeclaration: this.#processApiClass(astDeclaration, context); break; case ts.SyntaxKind.EnumDeclaration: this.#processApiEnum(astDeclaration, context); break; case ts.SyntaxKind.EnumMember: this.#processApiEnumMember(astDeclaration, context); break; case ts.SyntaxKind.FunctionDeclaration: this.#processApiFunction(astDeclaration, context); break; case ts.SyntaxKind.GetAccessor: this.#processApiProperty(astDeclaration, context); break; case ts.SyntaxKind.SetAccessor: this.#processApiProperty(astDeclaration, context); break; case ts.SyntaxKind.IndexSignature: this.#processApiIndexSignature(astDeclaration, context); break; case ts.SyntaxKind.InterfaceDeclaration: this.#processApiInterface(astDeclaration, context); break; case ts.SyntaxKind.MethodDeclaration: this.#processApiMethod(astDeclaration, context); break; case ts.SyntaxKind.MethodSignature: this.#processApiMethodSignature(astDeclaration, context); break; case ts.SyntaxKind.ModuleDeclaration: this.#processApiNamespace(astDeclaration, context); break; case ts.SyntaxKind.PropertyDeclaration: this.#processApiProperty(astDeclaration, context); break; case ts.SyntaxKind.PropertySignature: this.#processApiPropertySignature(astDeclaration, context); break; case ts.SyntaxKind.TypeAliasDeclaration: this.#processApiTypeAlias(astDeclaration, context); break; case ts.SyntaxKind.VariableDeclaration: // check for arrow functions in variable declaration const functionDeclaration = this.#tryFindFunctionDeclaration(astDeclaration); if (functionDeclaration) { this.#processApiFunction(astDeclaration, context, functionDeclaration); } else { this.#processApiVariable(astDeclaration, context); } break; default: // ignore unknown types } } #tryFindFunctionDeclaration(astDeclaration) { const children = astDeclaration.declaration.getChildren(astDeclaration.declaration.getSourceFile()); return children.find(ts.isFunctionTypeNode); } #processChildDeclarations(astDeclaration, context) { for (const childDeclaration of astDeclaration.children) { this.#processDeclaration(childDeclaration, { ...context, name: childDeclaration.astSymbol.localName }); } } #processApiCallSignature(astDeclaration, context) { const { parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiCallSignature.getContainerKey(overloadIndex); let apiCallSignature = parentApiItem.tryGetMemberByKey(containerKey); if (apiCallSignature === undefined) { const callSignature = astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (callSignature.type) { nodeTransforms.push({ node: callSignature.type, captureTokenRange: returnTypeTokenRange }); } const typeParameters = this.#captureTypeParameters(nodeTransforms, callSignature.typeParameters); const parameters = this.#captureParameters(nodeTransforms, callSignature.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(callSignature); apiCallSignature = new ApiCallSignature({ docComment, releaseTag, typeParameters, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, fileUrlPath }); parentApiItem.addMember(apiCallSignature); } } #processApiConstructor(astDeclaration, context) { const { parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiConstructor.getContainerKey(overloadIndex); let apiConstructor = parentApiItem.tryGetMemberByKey(containerKey); if (apiConstructor === undefined) { const constructorDeclaration = astDeclaration.declaration; const nodeTransforms = []; const parameters = this.#captureParameters(nodeTransforms, constructorDeclaration.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isProtected = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; const fileUrlPath = this.#getFileUrlPath(constructorDeclaration); apiConstructor = new ApiConstructor({ docComment, releaseTag, isProtected, parameters, overloadIndex, excerptTokens, fileUrlPath }); parentApiItem.addMember(apiConstructor); } } #processApiClass(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiClass.getContainerKey(name); let apiClass = parentApiItem.tryGetMemberByKey(containerKey); if (apiClass === undefined) { const classDeclaration = astDeclaration.declaration; const nodeTransforms = []; const typeParameters = this.#captureTypeParameters(nodeTransforms, classDeclaration.typeParameters); let extendsTokenRange = undefined; const implementsTokenRanges = []; for (const heritageClause of classDeclaration.heritageClauses || []) { if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { extendsTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (heritageClause.types.length > 0) { nodeTransforms.push({ node: heritageClause.types[0], captureTokenRange: extendsTokenRange }); } } else if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { for (const heritageType of heritageClause.types) { const implementsTokenRange = ExcerptBuilder.createEmptyTokenRange(); implementsTokenRanges.push(implementsTokenRange); nodeTransforms.push({ node: heritageType, captureTokenRange: implementsTokenRange }); } } } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isAbstract = (ts.getCombinedModifierFlags(classDeclaration) & ts.ModifierFlags.Abstract) !== 0; const fileUrlPath = this.#getFileUrlPath(classDeclaration); apiClass = new ApiClass({ name, isAbstract, docComment, releaseTag, excerptTokens, typeParameters, extendsTokenRange, implementsTokenRanges, isExported, fileUrlPath }); parentApiItem.addMember(apiClass); } this.#processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiClass }); } #processApiConstructSignature(astDeclaration, context) { const { parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiConstructSignature.getContainerKey(overloadIndex); let apiConstructSignature = parentApiItem.tryGetMemberByKey(containerKey); if (apiConstructSignature === undefined) { const constructSignature = astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (constructSignature.type) { nodeTransforms.push({ node: constructSignature.type, captureTokenRange: returnTypeTokenRange }); } const typeParameters = this.#captureTypeParameters(nodeTransforms, constructSignature.typeParameters); const parameters = this.#captureParameters(nodeTransforms, constructSignature.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(constructSignature); apiConstructSignature = new ApiConstructSignature({ docComment, releaseTag, typeParameters, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, fileUrlPath }); parentApiItem.addMember(apiConstructSignature); } } #processApiEnum(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiEnum.getContainerKey(name); let apiEnum = parentApiItem.tryGetMemberByKey(containerKey); if (apiEnum === undefined) { const excerptTokens = this.#buildExcerptTokens(astDeclaration, []); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const preserveMemberOrder = this.#collector.extractorConfig.enumMemberOrder === EnumMemberOrder.Preserve; const fileUrlPath = this.#getFileUrlPath(astDeclaration.declaration); apiEnum = new ApiEnum({ name, docComment, releaseTag, excerptTokens, preserveMemberOrder, isExported, fileUrlPath }); parentApiItem.addMember(apiEnum); } this.#processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiEnum }); } #processApiEnumMember(astDeclaration, context) { const { name, parentApiItem } = context; const containerKey = ApiEnumMember.getContainerKey(name); let apiEnumMember = parentApiItem.tryGetMemberByKey(containerKey); if (apiEnumMember === undefined) { const enumMember = astDeclaration.declaration; const nodeTransforms = []; let initializerTokenRange = undefined; if (enumMember.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodeTransforms.push({ node: enumMember.initializer, captureTokenRange: initializerTokenRange }); } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(enumMember); apiEnumMember = new ApiEnumMember({ name, docComment, releaseTag, excerptTokens, initializerTokenRange, fileUrlPath }); parentApiItem.addMember(apiEnumMember); } } #processApiFunction(astDeclaration, context, altFunctionDeclaration) { const { name, isExported, parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiFunction.getContainerKey(name, overloadIndex); let apiFunction = parentApiItem.tryGetMemberByKey(containerKey); if (apiFunction === undefined) { const functionDeclaration = altFunctionDeclaration ?? astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (functionDeclaration.type) { nodeTransforms.push({ node: functionDeclaration.type, captureTokenRange: returnTypeTokenRange }); } const typeParameters = this.#captureTypeParameters(nodeTransforms, functionDeclaration.typeParameters); const parameters = this.#captureParameters(nodeTransforms, functionDeclaration.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(functionDeclaration); apiFunction = new ApiFunction({ name, docComment, releaseTag, typeParameters, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, isExported, fileUrlPath }); parentApiItem.addMember(apiFunction); } } #processApiIndexSignature(astDeclaration, context) { const { parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiIndexSignature.getContainerKey(overloadIndex); let apiIndexSignature = parentApiItem.tryGetMemberByKey(containerKey); if (apiIndexSignature === undefined) { const indexSignature = astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodeTransforms.push({ node: indexSignature.type, captureTokenRange: returnTypeTokenRange }); const parameters = this.#captureParameters(nodeTransforms, indexSignature.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isReadonly = this.#isReadonly(astDeclaration); const fileUrlPath = this.#getFileUrlPath(indexSignature); apiIndexSignature = new ApiIndexSignature({ docComment, releaseTag, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, isReadonly, fileUrlPath }); parentApiItem.addMember(apiIndexSignature); } } #processApiInterface(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiInterface.getContainerKey(name); let apiInterface = parentApiItem.tryGetMemberByKey(containerKey); if (apiInterface === undefined) { const interfaceDeclaration = astDeclaration.declaration; const nodeTransforms = []; const typeParameters = this.#captureTypeParameters(nodeTransforms, interfaceDeclaration.typeParameters); const extendsTokenRanges = []; for (const heritageClause of interfaceDeclaration.heritageClauses || []) { if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { for (const heritageType of heritageClause.types) { const extendsTokenRange = ExcerptBuilder.createEmptyTokenRange(); extendsTokenRanges.push(extendsTokenRange); nodeTransforms.push({ node: heritageType, captureTokenRange: extendsTokenRange }); } } } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(interfaceDeclaration); apiInterface = new ApiInterface({ name, docComment, releaseTag, excerptTokens, typeParameters, extendsTokenRanges, isExported, fileUrlPath }); parentApiItem.addMember(apiInterface); } this.#processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiInterface }); } #processApiMethod(astDeclaration, context) { const { name, parentApiItem } = context; const isStatic = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiMethod.getContainerKey(name, isStatic, overloadIndex); let apiMethod = parentApiItem.tryGetMemberByKey(containerKey); if (apiMethod === undefined) { const methodDeclaration = astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (methodDeclaration.type) { nodeTransforms.push({ node: methodDeclaration.type, captureTokenRange: returnTypeTokenRange }); } const typeParameters = this.#captureTypeParameters(nodeTransforms, methodDeclaration.typeParameters); const parameters = this.#captureParameters(nodeTransforms, methodDeclaration.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { return; // trim out items marked as "@internal" or "@alpha" } const isOptional = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const isProtected = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; const isAbstract = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0; const fileUrlPath = this.#getFileUrlPath(methodDeclaration); apiMethod = new ApiMethod({ name, isAbstract, docComment, releaseTag, isProtected, isStatic, isOptional, typeParameters, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, fileUrlPath }); parentApiItem.addMember(apiMethod); } } #processApiMethodSignature(astDeclaration, context) { const { name, parentApiItem } = context; const overloadIndex = this.#collector.getOverloadIndex(astDeclaration); const containerKey = ApiMethodSignature.getContainerKey(name, overloadIndex); let apiMethodSignature = parentApiItem.tryGetMemberByKey(containerKey); if (apiMethodSignature === undefined) { const methodSignature = astDeclaration.declaration; const nodeTransforms = []; const returnTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (methodSignature.type) { nodeTransforms.push({ node: methodSignature.type, captureTokenRange: returnTypeTokenRange }); } const typeParameters = this.#captureTypeParameters(nodeTransforms, methodSignature.typeParameters); const parameters = this.#captureParameters(nodeTransforms, methodSignature.parameters); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const fileUrlPath = this.#getFileUrlPath(methodSignature); apiMethodSignature = new ApiMethodSignature({ name, docComment, releaseTag, isOptional, typeParameters, parameters, overloadIndex, excerptTokens, returnTypeTokenRange, fileUrlPath }); parentApiItem.addMember(apiMethodSignature); } } #processApiNamespace(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiNamespace.getContainerKey(name); let apiNamespace = parentApiItem.tryGetMemberByKey(containerKey); if (apiNamespace === undefined) { const excerptTokens = this.#buildExcerptTokens(astDeclaration, []); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(astDeclaration.declaration); apiNamespace = new ApiNamespace({ name, docComment, releaseTag, excerptTokens, isExported, fileUrlPath }); parentApiItem.addMember(apiNamespace); } this.#processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiNamespace }); } #processApiProperty(astDeclaration, context) { const { name, parentApiItem } = context; const isStatic = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; const containerKey = ApiProperty.getContainerKey(name, isStatic); let apiProperty = parentApiItem.tryGetMemberByKey(containerKey); if (apiProperty === undefined) { const declaration = astDeclaration.declaration; const nodeTransforms = []; const propertyTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); let propertyTypeNode; if (ts.isPropertyDeclaration(declaration) || ts.isGetAccessorDeclaration(declaration)) { propertyTypeNode = declaration.type; } if (ts.isSetAccessorDeclaration(declaration)) { // Note that TypeScript always reports an error if a setter does not have exactly one parameter. propertyTypeNode = declaration.parameters[0].type; } if (propertyTypeNode) { nodeTransforms.push({ node: propertyTypeNode, captureTokenRange: propertyTypeTokenRange }); } let initializerTokenRange = undefined; if (ts.isPropertyDeclaration(declaration) && declaration.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodeTransforms.push({ node: declaration.initializer, captureTokenRange: initializerTokenRange }); } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const isProtected = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; const isAbstract = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0; const isReadonly = this.#isReadonly(astDeclaration); const fileUrlPath = this.#getFileUrlPath(declaration); apiProperty = new ApiProperty({ name, docComment, releaseTag, isAbstract, isProtected, isStatic, isOptional, isReadonly, excerptTokens, propertyTypeTokenRange, initializerTokenRange, fileUrlPath }); parentApiItem.addMember(apiProperty); } else { // If the property was already declared before (via a merged interface declaration), // we assume its signature is identical, because the language requires that. } } #processApiPropertySignature(astDeclaration, context) { const { name, parentApiItem } = context; const containerKey = ApiPropertySignature.getContainerKey(name); let apiPropertySignature = parentApiItem.tryGetMemberByKey(containerKey); if (apiPropertySignature === undefined) { const propertySignature = astDeclaration.declaration; const nodeTransforms = []; const propertyTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (propertySignature.type) { nodeTransforms.push({ node: propertySignature.type, captureTokenRange: propertyTypeTokenRange }); } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const isReadonly = this.#isReadonly(astDeclaration); const fileUrlPath = this.#getFileUrlPath(propertySignature); apiPropertySignature = new ApiPropertySignature({ name, docComment, releaseTag, isOptional, excerptTokens, propertyTypeTokenRange, isReadonly, fileUrlPath }); parentApiItem.addMember(apiPropertySignature); } else { // If the property was already declared before (via a merged interface declaration), // we assume its signature is identical, because the language requires that. } } #processApiTypeAlias(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiTypeAlias.getContainerKey(name); let apiTypeAlias = parentApiItem.tryGetMemberByKey(containerKey); if (apiTypeAlias === undefined) { const typeAliasDeclaration = astDeclaration.declaration; const nodeTransforms = []; const typeParameters = this.#captureTypeParameters(nodeTransforms, typeAliasDeclaration.typeParameters); const typeTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodeTransforms.push({ node: typeAliasDeclaration.type, captureTokenRange: typeTokenRange }); const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const fileUrlPath = this.#getFileUrlPath(typeAliasDeclaration); apiTypeAlias = new ApiTypeAlias({ name, docComment, typeParameters, releaseTag, excerptTokens, typeTokenRange, isExported, fileUrlPath }); parentApiItem.addMember(apiTypeAlias); } } #processApiVariable(astDeclaration, context) { const { name, isExported, parentApiItem } = context; const containerKey = ApiVariable.getContainerKey(name); let apiVariable = parentApiItem.tryGetMemberByKey(containerKey); if (apiVariable === undefined) { const variableDeclaration = astDeclaration.declaration; const nodeTransforms = []; const variableTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (variableDeclaration.type) { nodeTransforms.push({ node: variableDeclaration.type, captureTokenRange: variableTypeTokenRange }); } let initializerTokenRange = undefined; if (variableDeclaration.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodeTransforms.push({ node: variableDeclaration.initializer, captureTokenRange: initializerTokenRange }); } const excerptTokens = this.#buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const releaseTag = apiItemMetadata.effectiveReleaseTag; const isReadonly = this.#isReadonly(astDeclaration); const fileUrlPath = this.#getFileUrlPath(variableDeclaration); apiVariable = new ApiVariable({ name, docComment, releaseTag, excerptTokens, variableTypeTokenRange, initializerTokenRange, isReadonly, isExported, fileUrlPath }); parentApiItem.addMember(apiVariable); } } /** * @param nodeTransforms - A list of child nodes whose token ranges we want to capture */ #buildExcerptTokens(astDeclaration, nodeTransforms) { const excerptTokens = []; // Build the main declaration ExcerptBuilder.addDeclaration(excerptTokens, astDeclaration, nodeTransforms, this.#referenceGenerator); const declarationMetadata = this.#collector.fetchDeclarationMetadata(astDeclaration); // Add any ancillary declarations for (const ancillaryDeclaration of declarationMetadata.ancillaryDeclarations) { ExcerptBuilder.addBlankLine(excerptTokens); ExcerptBuilder.addDeclaration(excerptTokens, ancillaryDeclaration, nodeTransforms, this.#referenceGenerator); } return excerptTokens; } #captureTypeParameters(nodeTransforms, typeParameterNodes) { const typeParameters = []; if (typeParameterNodes) { for (const typeParameter of typeParameterNodes) { const constraintTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (typeParameter.constraint) { nodeTransforms.push({ node: typeParameter.constraint, captureTokenRange: constraintTokenRange }); } const defaultTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (typeParameter.default) { nodeTransforms.push({ node: typeParameter.default, captureTokenRange: defaultTypeTokenRange }); } typeParameters.push({ typeParameterName: typeParameter.name.getText().trim(), constraintTokenRange, defaultTypeTokenRange }); } } return typeParameters; } #captureParameters(nodeTransforms, parameterNodes) { const parameters = []; DtsEmitHelpers.forEachParameterToNormalize(parameterNodes, (parameter, syntheticName) => { const parameterTypeTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (parameter.type) { nodeTransforms.push({ node: parameter.type, captureTokenRange: parameterTypeTokenRange }); } parameters.push({ parameterName: syntheticName ?? parameter.name.getText().trim(), parameterTypeTokenRange, isOptional: this.#collector.typeChecker.isOptionalParameter(parameter) }); if (syntheticName !== undefined) { // Replace the subexpression like "{ y, z }" with the synthesized parameter name nodeTransforms.push({ node: parameter.name, replacementText: syntheticName }); } }); return parameters; } #isReadonly(astDeclaration) { switch (astDeclaration.declaration.kind) { case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.IndexSignature: case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.PropertySignature: case ts.SyntaxKind.SetAccessor: case ts.SyntaxKind.VariableDeclaration: { const apiItemMetadata = this.#collector.fetchApiItemMetadata(astDeclaration); const docComment = apiItemMetadata.tsdocComment; const declarationMetadata = this.#collector.fetchDeclarationMetadata(astDeclaration); const hasReadonlyModifier = (astDeclaration.modifierFlags & ts.ModifierFlags.Readonly) !== 0; const hasReadonlyDocTag = !!docComment?.modifierTagSet?.hasTagName('@readonly'); const isGetterWithNoSetter = ts.isGetAccessorDeclaration(astDeclaration.declaration) && declarationMetadata.ancillaryDeclarations.length === 0; const isVarConst = ts.isVariableDeclaration(astDeclaration.declaration) && TypeScriptInternals.isVarConst(astDeclaration.declaration); return hasReadonlyModifier || hasReadonlyDocTag || isGetterWithNoSetter || isVarConst; } default: { // Readonly-ness does not make sense for any other declaration kind. return false; } } } #getFileUrlPath(declaration) { const sourceFile = declaration.getSourceFile(); const sourceLocation = this.#collector.sourceMapper.getSourceLocation({ sourceFile, pos: declaration.pos }); let result = path.relative(this.#collector.extractorConfig.projectFolder, sourceLocation.sourceFilePath); result = Path.convertToSlashes(result); return result; } } //# sourceMappingURL=ApiModelGenerator.js.map