diff --git a/apps/api-extractor/.vscode/launch.json b/apps/api-extractor/.vscode/launch.json index b9c74c3b15e..4a3f7bb4fa8 100644 --- a/apps/api-extractor/.vscode/launch.json +++ b/apps/api-extractor/.vscode/launch.json @@ -79,7 +79,7 @@ "run", "--local", "--config", - "./temp/configs/api-extractor-spanSorting.json" + "./temp/configs/api-extractor-destructuredParameters.json" ], "sourceMaps": true } diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts index 1ca3c340f9a..e03cfab218e 100644 --- a/apps/api-extractor/src/generators/ApiModelGenerator.ts +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -41,7 +41,7 @@ import { Path } from '@rushstack/node-core-library'; import type { Collector } from '../collector/Collector'; import type { ISourceLocation } from '../collector/SourceMapper'; import type { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ExcerptBuilder, type IExcerptBuilderNodeToCapture } from './ExcerptBuilder'; +import { ExcerptBuilder, type IExcerptBuilderNodeTransform } from './ExcerptBuilder'; import { AstSymbol } from '../analyzer/AstSymbol'; import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; @@ -51,6 +51,7 @@ import type { AstEntity } from '../analyzer/AstEntity'; import type { AstModule } from '../analyzer/AstModule'; import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; import type { ExtractorConfig } from '../api/ExtractorConfig'; +import { DtsEmitHelpers } from './DtsEmitHelpers'; interface IProcessAstEntityContext { name: string; @@ -319,22 +320,24 @@ export class ApiModelGenerator { const callSignature: ts.CallSignatureDeclaration = astDeclaration.declaration as ts.CallSignatureDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: callSignature.type, tokenRange: returnTypeTokenRange }); + if (callSignature.type) { + nodeTransforms.push({ node: callSignature.type, captureTokenRange: returnTypeTokenRange }); + } const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, callSignature.typeParameters ); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, callSignature.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -368,14 +371,14 @@ export class ApiModelGenerator { const constructorDeclaration: ts.ConstructorDeclaration = astDeclaration.declaration as ts.ConstructorDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, constructorDeclaration.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -405,10 +408,10 @@ export class ApiModelGenerator { if (apiClass === undefined) { const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, classDeclaration.typeParameters ); @@ -419,18 +422,18 @@ export class ApiModelGenerator { if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { extendsTokenRange = ExcerptBuilder.createEmptyTokenRange(); if (heritageClause.types.length > 0) { - nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange }); + nodeTransforms.push({ node: heritageClause.types[0], captureTokenRange: extendsTokenRange }); } } else if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { for (const heritageType of heritageClause.types) { const implementsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); implementsTokenRanges.push(implementsTokenRange); - nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange }); + nodeTransforms.push({ node: heritageType, captureTokenRange: implementsTokenRange }); } } } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -476,22 +479,24 @@ export class ApiModelGenerator { const constructSignature: ts.ConstructSignatureDeclaration = astDeclaration.declaration as ts.ConstructSignatureDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: constructSignature.type, tokenRange: returnTypeTokenRange }); + if (constructSignature.type) { + nodeTransforms.push({ node: constructSignature.type, captureTokenRange: returnTypeTokenRange }); + } const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, constructSignature.typeParameters ); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, constructSignature.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -556,15 +561,15 @@ export class ApiModelGenerator { if (apiEnumMember === undefined) { const enumMember: ts.EnumMember = astDeclaration.declaration as ts.EnumMember; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; let initializerTokenRange: IExcerptTokenRange | undefined = undefined; if (enumMember.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: enumMember.initializer, tokenRange: initializerTokenRange }); + nodeTransforms.push({ node: enumMember.initializer, captureTokenRange: initializerTokenRange }); } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -599,22 +604,24 @@ export class ApiModelGenerator { const functionDeclaration: ts.FunctionDeclaration = altFunctionDeclaration ?? (astDeclaration.declaration as ts.FunctionDeclaration); - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: functionDeclaration.type, tokenRange: returnTypeTokenRange }); + if (functionDeclaration.type) { + nodeTransforms.push({ node: functionDeclaration.type, captureTokenRange: returnTypeTokenRange }); + } const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, functionDeclaration.typeParameters ); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, functionDeclaration.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -650,17 +657,17 @@ export class ApiModelGenerator { const indexSignature: ts.IndexSignatureDeclaration = astDeclaration.declaration as ts.IndexSignatureDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: indexSignature.type, tokenRange: returnTypeTokenRange }); + nodeTransforms.push({ node: indexSignature.type, captureTokenRange: returnTypeTokenRange }); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, indexSignature.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -694,10 +701,10 @@ export class ApiModelGenerator { const interfaceDeclaration: ts.InterfaceDeclaration = astDeclaration.declaration as ts.InterfaceDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, interfaceDeclaration.typeParameters ); @@ -708,12 +715,12 @@ export class ApiModelGenerator { for (const heritageType of heritageClause.types) { const extendsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); extendsTokenRanges.push(extendsTokenRange); - nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange }); + nodeTransforms.push({ node: heritageType, captureTokenRange: extendsTokenRange }); } } } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -750,22 +757,24 @@ export class ApiModelGenerator { if (apiMethod === undefined) { const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: methodDeclaration.type, tokenRange: returnTypeTokenRange }); + if (methodDeclaration.type) { + nodeTransforms.push({ node: methodDeclaration.type, captureTokenRange: returnTypeTokenRange }); + } const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, methodDeclaration.typeParameters ); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, methodDeclaration.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -813,22 +822,24 @@ export class ApiModelGenerator { if (apiMethodSignature === undefined) { const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: methodSignature.type, tokenRange: returnTypeTokenRange }); + if (methodSignature.type) { + nodeTransforms.push({ node: methodSignature.type, captureTokenRange: returnTypeTokenRange }); + } const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, methodSignature.typeParameters ); const parameters: IApiParameterOptions[] = this._captureParameters( - nodesToCapture, + nodeTransforms, methodSignature.parameters ); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -894,7 +905,7 @@ export class ApiModelGenerator { if (apiProperty === undefined) { const declaration: ts.Declaration = astDeclaration.declaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); let propertyTypeNode: ts.TypeNode | undefined; @@ -908,15 +919,17 @@ export class ApiModelGenerator { propertyTypeNode = declaration.parameters[0].type; } - nodesToCapture.push({ node: propertyTypeNode, tokenRange: propertyTypeTokenRange }); + if (propertyTypeNode) { + nodeTransforms.push({ node: propertyTypeNode, captureTokenRange: propertyTypeTokenRange }); + } let initializerTokenRange: IExcerptTokenRange | undefined = undefined; if (ts.isPropertyDeclaration(declaration) && declaration.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: declaration.initializer, tokenRange: initializerTokenRange }); + nodeTransforms.push({ node: declaration.initializer, captureTokenRange: initializerTokenRange }); } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -962,12 +975,14 @@ export class ApiModelGenerator { if (apiPropertySignature === undefined) { const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: propertySignature.type, tokenRange: propertyTypeTokenRange }); + if (propertySignature.type) { + nodeTransforms.push({ node: propertySignature.type, captureTokenRange: propertyTypeTokenRange }); + } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -1007,17 +1022,17 @@ export class ApiModelGenerator { const typeAliasDeclaration: ts.TypeAliasDeclaration = astDeclaration.declaration as ts.TypeAliasDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( - nodesToCapture, + nodeTransforms, typeAliasDeclaration.typeParameters ); const typeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: typeAliasDeclaration.type, tokenRange: typeTokenRange }); + nodeTransforms.push({ node: typeAliasDeclaration.type, captureTokenRange: typeTokenRange }); - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -1049,18 +1064,23 @@ export class ApiModelGenerator { const variableDeclaration: ts.VariableDeclaration = astDeclaration.declaration as ts.VariableDeclaration; - const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const nodeTransforms: IExcerptBuilderNodeTransform[] = []; const variableTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: variableDeclaration.type, tokenRange: variableTypeTokenRange }); + if (variableDeclaration.type) { + nodeTransforms.push({ node: variableDeclaration.type, captureTokenRange: variableTypeTokenRange }); + } let initializerTokenRange: IExcerptTokenRange | undefined = undefined; if (variableDeclaration.initializer) { initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: variableDeclaration.initializer, tokenRange: initializerTokenRange }); + nodeTransforms.push({ + node: variableDeclaration.initializer, + captureTokenRange: initializerTokenRange + }); } - const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodeTransforms); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; @@ -1084,16 +1104,16 @@ export class ApiModelGenerator { } /** - * @param nodesToCapture - A list of child nodes whose token ranges we want to capture + * @param nodeTransforms - A list of child nodes whose token ranges we want to capture */ private _buildExcerptTokens( astDeclaration: AstDeclaration, - nodesToCapture: IExcerptBuilderNodeToCapture[] + nodeTransforms: IExcerptBuilderNodeTransform[] ): IExcerptToken[] { const excerptTokens: IExcerptToken[] = []; // Build the main declaration - ExcerptBuilder.addDeclaration(excerptTokens, astDeclaration, nodesToCapture, this._referenceGenerator); + ExcerptBuilder.addDeclaration(excerptTokens, astDeclaration, nodeTransforms, this._referenceGenerator); const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(astDeclaration); @@ -1103,7 +1123,7 @@ export class ApiModelGenerator { ExcerptBuilder.addDeclaration( excerptTokens, ancillaryDeclaration, - nodesToCapture, + nodeTransforms, this._referenceGenerator ); } @@ -1112,17 +1132,21 @@ export class ApiModelGenerator { } private _captureTypeParameters( - nodesToCapture: IExcerptBuilderNodeToCapture[], + nodeTransforms: IExcerptBuilderNodeTransform[], typeParameterNodes: ts.NodeArray | undefined ): IApiTypeParameterOptions[] { const typeParameters: IApiTypeParameterOptions[] = []; if (typeParameterNodes) { for (const typeParameter of typeParameterNodes) { const constraintTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: typeParameter.constraint, tokenRange: constraintTokenRange }); + if (typeParameter.constraint) { + nodeTransforms.push({ node: typeParameter.constraint, captureTokenRange: constraintTokenRange }); + } const defaultTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: typeParameter.default, tokenRange: defaultTypeTokenRange }); + if (typeParameter.default) { + nodeTransforms.push({ node: typeParameter.default, captureTokenRange: defaultTypeTokenRange }); + } typeParameters.push({ typeParameterName: typeParameter.name.getText().trim(), @@ -1135,19 +1159,31 @@ export class ApiModelGenerator { } private _captureParameters( - nodesToCapture: IExcerptBuilderNodeToCapture[], + nodeTransforms: IExcerptBuilderNodeTransform[], parameterNodes: ts.NodeArray ): IApiParameterOptions[] { const parameters: IApiParameterOptions[] = []; - for (const parameter of parameterNodes) { - const parameterTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); - nodesToCapture.push({ node: parameter.type, tokenRange: parameterTypeTokenRange }); - parameters.push({ - parameterName: parameter.name.getText().trim(), - parameterTypeTokenRange, - isOptional: this._collector.typeChecker.isOptionalParameter(parameter) - }); - } + + DtsEmitHelpers.forEachParameterToNormalize( + parameterNodes, + (parameter: ts.ParameterDeclaration, syntheticName: string | undefined): void => { + const parameterTypeTokenRange: IExcerptTokenRange = 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; } diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index b42293d0084..a18a62fe05f 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -25,6 +25,12 @@ import { ExtractorMessageId } from '../api/ExtractorMessageId'; import type { ApiReportVariant } from '../api/IConfigFile'; import type { SymbolMetadata } from '../collector/SymbolMetadata'; +interface IContext { + collector: Collector; + reportVariant: ApiReportVariant; + alreadyProcessedSignatures: Set; +} + export class ApiReportGenerator { private static _trimSpacesRegExp: RegExp = / +$/gm; @@ -92,6 +98,12 @@ export class ApiReportGenerator { } writer.ensureSkippedLine(); + const context: IContext = { + collector, + reportVariant, + alreadyProcessedSignatures: new Set() + }; + // Emit the regular declarations for (const entity of collector.entities) { const astEntity: AstEntity = entity.astEntity; @@ -152,7 +164,7 @@ export class ApiReportGenerator { if (apiItemMetadata.isPreapproved) { ApiReportGenerator._modifySpanForPreapproved(span); } else { - ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false, reportVariant); + ApiReportGenerator._modifySpan(span, entity, astDeclaration, false, context); } span.writeModifiedText(writer); @@ -276,13 +288,14 @@ export class ApiReportGenerator { * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. */ private static _modifySpan( - collector: Collector, span: Span, entity: CollectorEntity, astDeclaration: AstDeclaration, insideTypeLiteral: boolean, - reportVariant: ApiReportVariant + context: IContext ): void { + const { collector, reportVariant } = context; + // Should we process this declaration at all? if (!ApiReportGenerator._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) { span.modification.skipAll(); @@ -382,6 +395,19 @@ export class ApiReportGenerator { } break; + case ts.SyntaxKind.Parameter: + { + // (signature) -> SyntaxList -> Parameter + const signatureParent: Span | undefined = span.parent; + if (signatureParent) { + if (!context.alreadyProcessedSignatures.has(signatureParent)) { + context.alreadyProcessedSignatures.add(signatureParent); + DtsEmitHelpers.normalizeParameterNames(signatureParent); + } + } + } + break; + case ts.SyntaxKind.Identifier: const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityForNode( span.node as ts.Identifier @@ -414,12 +440,11 @@ export class ApiReportGenerator { astDeclaration, (childSpan, childAstDeclaration) => { ApiReportGenerator._modifySpan( - collector, childSpan, entity, childAstDeclaration, insideTypeLiteral, - reportVariant + context ); } ); @@ -460,14 +485,7 @@ export class ApiReportGenerator { } } - ApiReportGenerator._modifySpan( - collector, - child, - entity, - childAstDeclaration, - insideTypeLiteral, - reportVariant - ); + ApiReportGenerator._modifySpan(child, entity, childAstDeclaration, insideTypeLiteral, context); } } } diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 08dee27b812..7cde437bee8 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -190,4 +190,106 @@ export class DtsEmitHelpers { } return false; } + + /** + * Given an array that includes some parameter nodes, this returns an array of the same length; + * elements that are not undefined correspond to a parameter that should be renamed. + */ + public static forEachParameterToNormalize( + nodes: ArrayLike, + action: (parameter: ts.ParameterDeclaration, syntheticName: string | undefined) => void + ): void { + let actionIndex: number = 0; + + // Optimistically assume that no parameters need to be normalized + for (actionIndex = 0; actionIndex < nodes.length; ++actionIndex) { + const parameter: ts.Node = nodes[actionIndex]; + if (!ts.isParameter(parameter)) { + continue; + } + action(parameter, undefined); + if (ts.isObjectBindingPattern(parameter.name) || ts.isArrayBindingPattern(parameter.name)) { + // Our optimistic assumption was not true; we'll need to stop and calculate alreadyUsedNames + break; + } + } + + if (actionIndex === nodes.length) { + // Our optimistic assumption was true + return; + } + + // First, calculate alreadyUsedNames + const alreadyUsedNames: string[] = []; + + for (let index: number = 0; index < nodes.length; ++index) { + const parameter: ts.Node = nodes[index]; + if (!ts.isParameter(parameter)) { + continue; + } + + if (!(ts.isObjectBindingPattern(parameter.name) || ts.isArrayBindingPattern(parameter.name))) { + alreadyUsedNames.push(parameter.name.text.trim()); + } + } + + // Now continue with the rest of the actions + for (; actionIndex < nodes.length; ++actionIndex) { + const parameter: ts.Node = nodes[actionIndex]; + if (!ts.isParameter(parameter)) { + continue; + } + + if (ts.isObjectBindingPattern(parameter.name) || ts.isArrayBindingPattern(parameter.name)) { + // Examples: + // + // function f({ y, z }: { y: string, z: string }) + // ---> function f(input: { y: string, z: string }) + // + // function f(x: number, [a, b]: [number, number]) + // ---> function f(x: number, input: [number, number]) + // + // Example of a naming collision: + // + // function f({ a }: { a: string }, { b }: { b: string }, input2: string) + // ---> function f(input: { a: string }, input3: { b: string }, input2: string) + const baseName: string = 'input'; + let counter: number = 2; + + let syntheticName: string = baseName; + while (alreadyUsedNames.includes(syntheticName)) { + syntheticName = `${baseName}${counter++}`; + } + alreadyUsedNames.push(syntheticName); + + action(parameter, syntheticName); + } else { + action(parameter, undefined); + } + } + } + + public static normalizeParameterNames(signatureSpan: Span): void { + const syntheticNamesByNode: Map = new Map(); + + DtsEmitHelpers.forEachParameterToNormalize( + signatureSpan.node.getChildren(), + (parameter: ts.ParameterDeclaration, syntheticName: string | undefined): void => { + if (syntheticName !== undefined) { + syntheticNamesByNode.set(parameter.name, syntheticName); + } + } + ); + + if (syntheticNamesByNode.size > 0) { + signatureSpan.forEach((childSpan: Span): void => { + const syntheticName: string | undefined = syntheticNamesByNode.get(childSpan.node); + if (syntheticName !== undefined) { + childSpan.modification.prefix = syntheticName; + childSpan.modification.suffix = ''; + childSpan.modification.omitChildren = true; + } + }); + } + } } diff --git a/apps/api-extractor/src/generators/ExcerptBuilder.ts b/apps/api-extractor/src/generators/ExcerptBuilder.ts index 8b10b4674d1..ce1662c789a 100644 --- a/apps/api-extractor/src/generators/ExcerptBuilder.ts +++ b/apps/api-extractor/src/generators/ExcerptBuilder.ts @@ -17,16 +17,22 @@ import type { AstDeclaration } from '../analyzer/AstDeclaration'; /** * Used to provide ExcerptBuilder with a list of nodes whose token range we want to capture. */ -export interface IExcerptBuilderNodeToCapture { +export interface IExcerptBuilderNodeTransform { /** - * The node to capture + * The node to process */ - node: ts.Node | undefined; + node: ts.Node; + /** - * The token range whose startIndex/endIndex will be overwritten with the indexes for the - * tokens corresponding to IExcerptBuilderNodeToCapture.node + * A token range whose startIndex/endIndex will be overwritten with the indexes for the + * tokens corresponding to IExcerptBuilderNodeTransform.node */ - tokenRange: IExcerptTokenRange; + captureTokenRange?: IExcerptTokenRange; + + /** + * Text that will replace the text of the given node during emit. + */ + replacementText?: string; } /** @@ -51,7 +57,7 @@ interface IBuildSpanState { */ stopBeforeChildKind: ts.SyntaxKind | undefined; - tokenRangesByNode: Map; + transformsByNode: Map; /** * Tracks whether the last appended token was a separator. If so, and we're in the middle of @@ -80,12 +86,12 @@ export class ExcerptBuilder { /** * Appends the signature for the specified `AstDeclaration` to the `excerptTokens` list. * @param excerptTokens - The target token list to append to - * @param nodesToCapture - A list of child nodes whose token ranges we want to capture + * @param nodeTransforms - A list of child nodes whose token ranges we want to capture */ public static addDeclaration( excerptTokens: IExcerptToken[], astDeclaration: AstDeclaration, - nodesToCapture: IExcerptBuilderNodeToCapture[], + nodeTransforms: IExcerptBuilderNodeTransform[], referenceGenerator: DeclarationReferenceGenerator ): void { let stopBeforeChildKind: ts.SyntaxKind | undefined = undefined; @@ -105,10 +111,12 @@ export class ExcerptBuilder { const span: Span = new Span(astDeclaration.declaration); - const tokenRangesByNode: Map = new Map(); - for (const excerpt of nodesToCapture || []) { - if (excerpt.node) { - tokenRangesByNode.set(excerpt.node, excerpt.tokenRange); + const transformsByNode: Map = new Map(); + const captureTokenRanges: IExcerptTokenRange[] = []; + for (const nodeTransform of nodeTransforms || []) { + transformsByNode.set(nodeTransform.node, nodeTransform); + if (nodeTransform.captureTokenRange) { + captureTokenRanges.push(nodeTransform.captureTokenRange); } } @@ -116,16 +124,18 @@ export class ExcerptBuilder { referenceGenerator: referenceGenerator, startingNode: span.node, stopBeforeChildKind, - tokenRangesByNode, + transformsByNode: transformsByNode, lastAppendedTokenIsSeparator: false }); - ExcerptBuilder._condenseTokens(excerptTokens, [...tokenRangesByNode.values()]); + + ExcerptBuilder._condenseTokens(excerptTokens, captureTokenRanges); } public static createEmptyTokenRange(): IExcerptTokenRange { return { startIndex: 0, endIndex: 0 }; } + /** @returns false if we encountered a token that causes iteration to stop. */ private static _buildSpan(excerptTokens: IExcerptToken[], span: Span, state: IBuildSpanState): boolean { if (span.kind === ts.SyntaxKind.JSDocComment) { // Discard any comments @@ -133,10 +143,30 @@ export class ExcerptBuilder { } // Can this node start a excerpt? - const capturedTokenRange: IExcerptTokenRange | undefined = state.tokenRangesByNode.get(span.node); + const transform: IExcerptBuilderNodeTransform | undefined = state.transformsByNode.get(span.node); + + let captureTokenRange: IExcerptTokenRange | undefined = undefined; + + if (transform) { + captureTokenRange = transform.captureTokenRange; + if (transform.replacementText !== undefined) { + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: transform.replacementText + }); + state.lastAppendedTokenIsSeparator = false; + + if (captureTokenRange) { + captureTokenRange.startIndex = excerptTokens.length; + captureTokenRange.endIndex = captureTokenRange.startIndex + 1; + } + return true; + } + } + let excerptStartIndex: number = 0; - if (capturedTokenRange) { + if (captureTokenRange) { // We will assign capturedTokenRange.startIndex to be the index of the next token to be appended excerptStartIndex = excerptTokens.length; } @@ -187,8 +217,8 @@ export class ExcerptBuilder { } // Are we building a excerpt? If so, set its range - if (capturedTokenRange) { - capturedTokenRange.startIndex = excerptStartIndex; + if (captureTokenRange) { + captureTokenRange.startIndex = excerptStartIndex; // We will assign capturedTokenRange.startIndex to be the index after the last token // that was appended so far. However, if the last appended token was a separator, omit @@ -198,7 +228,7 @@ export class ExcerptBuilder { excerptEndIndex--; } - capturedTokenRange.endIndex = excerptEndIndex; + captureTokenRange.endIndex = excerptEndIndex; } return true; diff --git a/build-tests/api-documenter-scenarios/.vscode/launch.json b/build-tests/api-documenter-scenarios/.vscode/launch.json new file mode 100644 index 00000000000..912e1303501 --- /dev/null +++ b/build-tests/api-documenter-scenarios/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "heft build (runScenarios.js)", + "program": "${workspaceFolder}/node_modules/@rushstack/heft/lib/start.js", + "cwd": "${workspaceFolder}", + "args": ["--debug", "build"], + "console": "integratedTerminal", + "sourceMaps": true + }, + { + "type": "node", + "request": "launch", + "name": "api-documenter [scenario]", + "program": "${workspaceFolder}/node_modules/@microsoft/api-documenter/lib/start.js", + "cwd": "${workspaceFolder}/temp/configs", + "args": [ + "generate", + "--input-folder", + "${workspaceFolder}/temp/etc/commentedDestructure", + "--output-folder", + "${workspaceFolder}/temp/etc/commentedDestructure/markdown" + ], + "console": "integratedTerminal", + "sourceMaps": true + } + ] +} diff --git a/build-tests/api-documenter-scenarios/src/runScenarios.ts b/build-tests/api-documenter-scenarios/src/runScenarios.ts index 1b89ed09e1e..ee0e89b8479 100644 --- a/build-tests/api-documenter-scenarios/src/runScenarios.ts +++ b/build-tests/api-documenter-scenarios/src/runScenarios.ts @@ -13,60 +13,56 @@ export async function runAsync(runScriptOptions: IRunScriptOptions): Promise { - // API Documenter will always look for a config file in the same place (it cannot be configured), so this script - // manually overwrites the API documenter config for each scenario. This is in contrast to the separate config files - // created when invoking API Extractor above. - const apiDocumenterOverridesPath: string = `${buildFolderPath}/src/${scenarioFolderName}/config/api-documenter-overrides.json`; - let apiDocumenterJsonOverrides: {} | undefined; - try { - apiDocumenterJsonOverrides = await JsonFile.loadAsync(apiDocumenterOverridesPath); - } catch (e) { - if (!FileSystem.isNotExistError(e)) { - throw e; - } + await runScenariosAsync(runScriptOptions, { + libFolderPath: __dirname, + afterApiExtractorAsync: async (scenarioFolderName: string) => { + // API Documenter will always look for a config file in the same place (it cannot be configured), so this script + // manually overwrites the API documenter config for each scenario. This is in contrast to the separate config files + // created when invoking API Extractor above. + const apiDocumenterOverridesPath: string = `${buildFolderPath}/src/${scenarioFolderName}/config/api-documenter-overrides.json`; + let apiDocumenterJsonOverrides: {} | undefined; + try { + apiDocumenterJsonOverrides = await JsonFile.loadAsync(apiDocumenterOverridesPath); + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; } + } - const apiDocumenterJson: {} = { - $schema: 'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-documenter.schema.json', - outputTarget: 'markdown', - tableOfContents: {}, - ...apiDocumenterJsonOverrides - }; + const apiDocumenterJson: {} = { + $schema: 'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-documenter.schema.json', + outputTarget: 'markdown', + tableOfContents: {}, + ...apiDocumenterJsonOverrides + }; - await JsonFile.saveAsync(apiDocumenterJson, apiDocumenterJsonPath, { ensureFolderExists: true }); + await JsonFile.saveAsync(apiDocumenterJson, apiDocumenterJsonPath, { ensureFolderExists: true }); - // TODO: Ensure that the checked-in files are up-to-date - // Run the API Documenter command-line - const childProcess: ChildProcess = Executable.spawn( - process.argv0, - [ - 'node_modules/@microsoft/api-documenter/lib/start', - 'generate', - `--input-folder`, - `temp/etc/${scenarioFolderName}`, - '--output-folder', - `temp/etc/${scenarioFolderName}/markdown` - ], - { - stdio: 'inherit' - } - ); + // TODO: Ensure that the checked-in files are up-to-date + // Run the API Documenter command-line + const childProcess: ChildProcess = Executable.spawn( + process.argv0, + [ + `${buildFolderPath}/node_modules/@microsoft/api-documenter/lib/start`, + 'generate', + `--input-folder`, + `${buildFolderPath}/temp/etc/${scenarioFolderName}`, + '--output-folder', + `${buildFolderPath}/temp/etc/${scenarioFolderName}/markdown` + ], + { + stdio: 'inherit', + // api-documenter will find api-documenter.json in this folder: + currentWorkingDirectory: `${buildFolderPath}/temp/configs` + } + ); - await Executable.waitForExitAsync(childProcess, { - throwOnNonZeroExitCode: true, - throwOnSignal: true - }); - } - }); - } finally { - // Delete the transient `api-documenter.json` file before completing, as it'll just be whatever the last scenario - // was, and shouldn't be committed. - await FileSystem.deleteFileAsync(apiDocumenterJsonPath); - } + await Executable.waitForExitAsync(childProcess, { + throwOnNonZeroExitCode: true, + throwOnSignal: true + }); + } + }); } diff --git a/build-tests/api-documenter-test/etc/api-documenter-test.api.md b/build-tests/api-documenter-test/etc/api-documenter-test.api.md index 4b6989eff89..4c1077d66a6 100644 --- a/build-tests/api-documenter-test/etc/api-documenter-test.api.md +++ b/build-tests/api-documenter-test/etc/api-documenter-test.api.md @@ -133,7 +133,7 @@ export interface IDocInterface3 { // @public export interface IDocInterface4 { - Context: ({ children }: { + Context: (input: { children: string; }) => boolean; generic: Generic; diff --git a/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..e3e5d39c9f9 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.json @@ -0,0 +1,608 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1011, + "oldestForwardsCompatibleVersion": 1001, + "tsdocConfig": { + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "noStandardTags": true, + "tagDefinitions": [ + { + "tagName": "@alpha", + "syntaxKind": "modifier" + }, + { + "tagName": "@beta", + "syntaxKind": "modifier" + }, + { + "tagName": "@defaultValue", + "syntaxKind": "block" + }, + { + "tagName": "@decorator", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@deprecated", + "syntaxKind": "block" + }, + { + "tagName": "@eventProperty", + "syntaxKind": "modifier" + }, + { + "tagName": "@example", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@experimental", + "syntaxKind": "modifier" + }, + { + "tagName": "@inheritDoc", + "syntaxKind": "inline" + }, + { + "tagName": "@internal", + "syntaxKind": "modifier" + }, + { + "tagName": "@label", + "syntaxKind": "inline" + }, + { + "tagName": "@link", + "syntaxKind": "inline", + "allowMultiple": true + }, + { + "tagName": "@override", + "syntaxKind": "modifier" + }, + { + "tagName": "@packageDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@param", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@privateRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@public", + "syntaxKind": "modifier" + }, + { + "tagName": "@readonly", + "syntaxKind": "modifier" + }, + { + "tagName": "@remarks", + "syntaxKind": "block" + }, + { + "tagName": "@returns", + "syntaxKind": "block" + }, + { + "tagName": "@sealed", + "syntaxKind": "modifier" + }, + { + "tagName": "@see", + "syntaxKind": "block" + }, + { + "tagName": "@throws", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@typeParam", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@virtual", + "syntaxKind": "modifier" + }, + { + "tagName": "@jsx", + "syntaxKind": "block" + }, + { + "tagName": "@jsxRuntime", + "syntaxKind": "block" + }, + { + "tagName": "@jsxFrag", + "syntaxKind": "block" + }, + { + "tagName": "@jsxImportSource", + "syntaxKind": "block" + }, + { + "tagName": "@betaDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@internalRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@preapproved", + "syntaxKind": "modifier" + } + ], + "supportForTags": { + "@alpha": true, + "@beta": true, + "@defaultValue": true, + "@decorator": true, + "@deprecated": true, + "@eventProperty": true, + "@example": true, + "@experimental": true, + "@inheritDoc": true, + "@internal": true, + "@label": true, + "@link": true, + "@override": true, + "@packageDocumentation": true, + "@param": true, + "@privateRemarks": true, + "@public": true, + "@readonly": true, + "@remarks": true, + "@returns": true, + "@sealed": true, + "@see": true, + "@throws": true, + "@typeParam": true, + "@virtual": true, + "@betaDocumentation": true, + "@internalRemarks": true, + "@preapproved": true + }, + "reportUnsupportedHtmlElements": false + } + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testArray:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testArray(input: " + }, + { + "kind": "Content", + "text": "[number, number]" + }, + { + "kind": "Content", + "text": ", last: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "[x, y]", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "last", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "name": "testArray" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testNameConflict:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testNameConflict(input2: " + }, + { + "kind": "Content", + "text": "[number, number]" + }, + { + "kind": "Content", + "text": ", input: " + }, + { + "kind": "Content", + "text": "boolean" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "[x, y]", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input2", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "name": "testNameConflict" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testNameConflict2:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testNameConflict2(input: " + }, + { + "kind": "Content", + "text": "{\n x: number;\n}" + }, + { + "kind": "Content", + "text": ", input3: " + }, + { + "kind": "Content", + "text": "{\n y: number;\n}" + }, + { + "kind": "Content", + "text": ", input2: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "{ x }", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "input3", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "input2", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + } + ], + "name": "testNameConflict2" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testObject:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testObject(first: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", input: " + }, + { + "kind": "Content", + "text": "{\n x: number;\n y: number;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "first", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "{ x, y }", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "name": "testObject" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testObjects:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testObjects(input: " + }, + { + "kind": "Content", + "text": "{\n x: number;\n}" + }, + { + "kind": "Content", + "text": ", input2: " + }, + { + "kind": "Content", + "text": "{\n y: number;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "{ x }", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "input2", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "name": "testObjects" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!testObjectWithComments:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function testObjectWithComments(input: " + }, + { + "kind": "Content", + "text": "{\n x: number;\n y: number;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/destructuredParameters/index.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "{ x, // slash P3\ny }", + "parameterTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "testObjectWithComments" + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..9cf44eb2703 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/destructuredParameters/api-extractor-scenarios.api.md @@ -0,0 +1,41 @@ +## API Report File for "api-extractor-scenarios" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export function testArray(input: [number, number], last: string): void; + +// @public (undocumented) +export function testNameConflict(input2: [number, number], input: boolean): void; + +// @public (undocumented) +export function testNameConflict2(input: { + x: number; +}, input3: { + y: number; +}, input2: string): void; + +// @public (undocumented) +export function testObject(first: string, input: { + x: number; + y: number; +}): void; + +// @public (undocumented) +export function testObjects(input: { + x: number; +}, input2: { + y: number; +}): void; + +// @public (undocumented) +export function testObjectWithComments(input: { + x: number; + y: number; +}): void; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/destructuredParameters/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/destructuredParameters/rollup.d.ts new file mode 100644 index 00000000000..ff81ffe1caf --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/destructuredParameters/rollup.d.ts @@ -0,0 +1,34 @@ +/** @public */ +export declare function testArray([x, y]: [number, number], last: string): void; + +/** @public */ +export declare function testNameConflict([x, y]: [number, number], input: boolean): void; + +/** @public */ +export declare function testNameConflict2({ x }: { + x: number; +}, { y }: { + y: number; +}, input2: string): void; + +/** @public */ +export declare function testObject(first: string, { x, y }: { + x: number; + y: number; +}): void; + +/** @public */ +export declare function testObjects({ x }: { + x: number; +}, { y }: { + y: number; +}): void; + +/** @public */ +export declare function testObjectWithComments({ x, // slash P3 + y }: { + x: number; + y: number; +}): void; + +export { } diff --git a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.json index a25d98c8424..5216041af41 100644 --- a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.json @@ -656,16 +656,7 @@ "excerptTokens": [ { "kind": "Content", - "text": "export declare function someFunction7({ " - }, - { - "kind": "Reference", - "text": "then", - "canonicalReference": "!Promise#then" - }, - { - "kind": "Content", - "text": ": then2 }: " + "text": "export declare function someFunction7(input: " }, { "kind": "Reference", @@ -696,8 +687,8 @@ ], "fileUrlPath": "src/referenceTokens/index.ts", "returnTypeTokenRange": { - "startIndex": 6, - "endIndex": 8 + "startIndex": 4, + "endIndex": 6 }, "releaseTag": "Public", "overloadIndex": 1, @@ -705,8 +696,16 @@ { "parameterName": "{ then: then2 }", "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 5 + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 }, "isOptional": false } @@ -720,16 +719,7 @@ "excerptTokens": [ { "kind": "Content", - "text": "export declare function someFunction8({ " - }, - { - "kind": "Reference", - "text": "prop", - "canonicalReference": "api-extractor-lib2-test!Lib2Class#prop" - }, - { - "kind": "Content", - "text": ": prop2 }: " + "text": "export declare function someFunction8(input: " }, { "kind": "Reference", @@ -751,8 +741,8 @@ ], "fileUrlPath": "src/referenceTokens/index.ts", "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "overloadIndex": 1, @@ -760,8 +750,16 @@ { "parameterName": "{ prop: prop2 }", "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 }, "isOptional": false } @@ -775,16 +773,7 @@ "excerptTokens": [ { "kind": "Content", - "text": "export declare function someFunction9({ " - }, - { - "kind": "Reference", - "text": "prop", - "canonicalReference": "api-extractor-scenarios!SomeInterface1#prop" - }, - { - "kind": "Content", - "text": ": prop2 }: " + "text": "export declare function someFunction9(input: " }, { "kind": "Reference", @@ -806,8 +795,8 @@ ], "fileUrlPath": "src/referenceTokens/index.ts", "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 3, + "endIndex": 4 }, "releaseTag": "Public", "overloadIndex": 1, @@ -815,8 +804,16 @@ { "parameterName": "{ prop: prop2 }", "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 0, + "endIndex": 0 + }, + "isOptional": false + }, + { + "parameterName": "input", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 }, "isOptional": false } diff --git a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md index 31940327106..16cf6cb3149 100644 --- a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md @@ -65,13 +65,13 @@ export function someFunction5(): SomeEnum.A; export function someFunction6(): typeof SomeClass1.staticProp; // @public -export function someFunction7({ then: then2 }: Promise): typeof Date.prototype.getDate; +export function someFunction7(input: Promise): typeof Date.prototype.getDate; // @public -export function someFunction8({ prop: prop2 }: Lib2Class): void; +export function someFunction8(input: Lib2Class): void; // @public -export function someFunction9({ prop: prop2 }: SomeInterface1): void; +export function someFunction9(input: SomeInterface1): void; // @public (undocumented) export interface SomeInterface1 { diff --git a/build-tests/api-extractor-scenarios/src/destructuredParameters/index.ts b/build-tests/api-extractor-scenarios/src/destructuredParameters/index.ts new file mode 100644 index 00000000000..021cda4b5dd --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/destructuredParameters/index.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** @public */ +export function testObject(first: string, { x, y }: { x: number; y: number }): void {} + +/** @public */ +export function testArray([x, y]: [number, number], last: string): void {} + +/** @public */ +export function testObjects({ x }: { x: number }, { y }: { y: number }): void {} + +/** @public */ +export function testNameConflict([x, y]: [number, number], input: boolean): void {} + +/** @public */ +export function testNameConflict2({ x }: { x: number }, { y }: { y: number }, input2: string): void {} + +/** @public */ +export function testObjectWithComments( + // slash P1 + { + // slash P2 + x, // slash P3 + // slash P4 + y // slash P5 + // slash P6 + }: // slash T1 + { + // slash T2 + x: number; // slash T3 + // slash T4 + y: number; // slash T5 + // slash T6 + } // slash T7 + // slash T8 +): void {} diff --git a/build-tests/api-extractor-test-05/dist/tsdoc-metadata.json b/build-tests/api-extractor-test-05/dist/tsdoc-metadata.json index 74e45be236b..c017748cd67 100644 --- a/build-tests/api-extractor-test-05/dist/tsdoc-metadata.json +++ b/build-tests/api-extractor-test-05/dist/tsdoc-metadata.json @@ -5,7 +5,7 @@ "toolPackages": [ { "packageName": "@microsoft/api-extractor", - "packageVersion": "7.55.2" + "packageVersion": "7.55.5" } ] } diff --git a/common/changes/@microsoft/api-extractor/octogonz-api-extractor-destructured-params_2026-01-22-07-11.json b/common/changes/@microsoft/api-extractor/octogonz-api-extractor-destructured-params_2026-01-22-07-11.json new file mode 100644 index 00000000000..036d119e979 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/octogonz-api-extractor-destructured-params_2026-01-22-07-11.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Fix an issue where destructured parameters produced an incorrect parameter name", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file diff --git a/common/reviews/api/localization-utilities.api.md b/common/reviews/api/localization-utilities.api.md index d42a0efbf98..762259c0837 100644 --- a/common/reviews/api/localization-utilities.api.md +++ b/common/reviews/api/localization-utilities.api.md @@ -96,10 +96,10 @@ export interface ITypingsGeneratorOptions extends ITypingsGeneratorBaseOptions { export function parseLocFile(options: IParseLocFileOptions): ILocalizationFile; // @public (undocumented) -export function parseLocJson({ content, filePath, ignoreString }: IParseFileOptions): ILocalizationFile; +export function parseLocJson(input: IParseFileOptions): ILocalizationFile; // @public (undocumented) -export function parseResJson({ content, ignoreString, filePath }: IParseFileOptions): ILocalizationFile; +export function parseResJson(input: IParseFileOptions): ILocalizationFile; // @public (undocumented) export function parseResx(options: IParseResxOptions): ILocalizationFile; diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index e0f49f952c7..5cd278ad597 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1003,11 +1003,11 @@ export class _OperationMetadataManager { readonly logFilenameIdentifier: string; get metadataFolderPath(): string; // (undocumented) - saveAsync({ durationInSeconds, cobuildContextId, cobuildRunnerId, logPath, errorLogPath, logChunksPath }: _IOperationMetadata): Promise; + saveAsync(input: _IOperationMetadata): Promise; // (undocumented) readonly stateFile: _OperationStateFile; // (undocumented) - tryRestoreAsync({ terminal, terminalProvider, errorLogPath, cobuildContextId, cobuildRunnerId }: { + tryRestoreAsync(input: { terminalProvider: ITerminalProvider; terminal: ITerminal; errorLogPath: string; diff --git a/common/reviews/api/rush-themed-ui.api.md b/common/reviews/api/rush-themed-ui.api.md index 7e98f208160..ed51c785cff 100644 --- a/common/reviews/api/rush-themed-ui.api.md +++ b/common/reviews/api/rush-themed-ui.api.md @@ -7,10 +7,10 @@ import { default as React_2 } from 'react'; // @public -export const Button: ({ children, disabled, onClick }: IButtonProps) => React_2.ReactElement; +export const Button: (input: IButtonProps) => React_2.ReactElement; // @public -export const Checkbox: ({ label, isChecked, onChecked }: ICheckboxProps) => React_2.ReactElement; +export const Checkbox: (input: ICheckboxProps) => React_2.ReactElement; // @public export interface IButtonProps { @@ -45,7 +45,7 @@ export interface IInputProps { } // @public -export const Input: ({ value, placeholder, onChange, type }: IInputProps) => React_2.ReactElement; +export const Input: (input: IInputProps) => React_2.ReactElement; // @public export interface IScrollAreaProps { @@ -92,13 +92,13 @@ export interface ITextProps { } // @public -export const ScrollArea: ({ children }: IScrollAreaProps) => React_2.ReactElement; +export const ScrollArea: (input: IScrollAreaProps) => React_2.ReactElement; // @public -export const Tabs: ({ items, def, value, onChange, renderChildren }: ITabsProps) => React_2.ReactElement; +export const Tabs: (input: ITabsProps) => React_2.ReactElement; // @public -const Text_2: ({ type, bold, children, className, size }: ITextProps) => React_2.ReactElement; +const Text_2: (input: ITextProps) => React_2.ReactElement; export { Text_2 as Text } // @public