Skip to content

[api-extractor] Fix an issue with preservation of triple-slash references. #5131

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/api-extractor/src/analyzer/AstModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import type { AstEntity } from './AstEntity';
/**
* Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo}
*/
export class AstModuleExportInfo {
public readonly exportedLocalEntities: Map<string, AstEntity> = new Map<string, AstEntity>();
public readonly starExportedExternalModules: Set<AstModule> = new Set<AstModule>();
export interface IAstModuleExportInfo {
readonly visitedAstModules: Set<AstModule>;
readonly exportedLocalEntities: Map<string, AstEntity>;
readonly starExportedExternalModules: Set<AstModule>;
}

/**
Expand Down Expand Up @@ -64,7 +65,7 @@ export class AstModule {
/**
* Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`.
*/
public astModuleExportInfo: AstModuleExportInfo | undefined;
public astModuleExportInfo: IAstModuleExportInfo | undefined;

public constructor(options: IAstModuleOptions) {
this.sourceFile = options.sourceFile;
Expand Down
6 changes: 3 additions & 3 deletions apps/api-extractor/src/analyzer/AstNamespaceImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import type * as ts from 'typescript';

import type { AstModule, AstModuleExportInfo } from './AstModule';
import type { AstModule, IAstModuleExportInfo } from './AstModule';
import { AstSyntheticEntity } from './AstEntity';
import type { Collector } from '../collector/Collector';

Expand Down Expand Up @@ -87,8 +87,8 @@ export class AstNamespaceImport extends AstSyntheticEntity {
return this.namespaceName;
}

public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(
public fetchAstModuleExportInfo(collector: Collector): IAstModuleExportInfo {
const astModuleExportInfo: IAstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(
this.astModule
);
return astModuleExportInfo;
Expand Down
4 changes: 2 additions & 2 deletions apps/api-extractor/src/analyzer/AstSymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { type PackageJsonLookup, InternalError } from '@rushstack/node-core-libr
import { AstDeclaration } from './AstDeclaration';
import { TypeScriptHelpers } from './TypeScriptHelpers';
import { AstSymbol } from './AstSymbol';
import type { AstModule, AstModuleExportInfo } from './AstModule';
import type { AstModule, IAstModuleExportInfo } from './AstModule';
import { PackageMetadataManager } from './PackageMetadataManager';
import { ExportAnalyzer } from './ExportAnalyzer';
import type { AstEntity } from './AstEntity';
Expand Down Expand Up @@ -124,7 +124,7 @@ export class AstSymbolTable {
/**
* This crawls the specified entry point and collects the full set of exported AstSymbols.
*/
public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(astModule: AstModule): IAstModuleExportInfo {
return this._exportAnalyzer.fetchAstModuleExportInfo(astModule);
}

Expand Down
27 changes: 14 additions & 13 deletions apps/api-extractor/src/analyzer/ExportAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { InternalError } from '@rushstack/node-core-library';
import { TypeScriptHelpers } from './TypeScriptHelpers';
import { AstSymbol } from './AstSymbol';
import { AstImport, type IAstImportOptions, AstImportKind } from './AstImport';
import { AstModule, AstModuleExportInfo } from './AstModule';
import { AstModule, type IAstModuleExportInfo } from './AstModule';
import { TypeScriptInternals } from './TypeScriptInternals';
import { SourceFileLocationFormatter } from './SourceFileLocationFormatter';
import type { IFetchAstSymbolOptions } from './AstSymbolTable';
Expand Down Expand Up @@ -237,15 +237,19 @@ export class ExportAnalyzer {
/**
* Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}.
*/
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): IAstModuleExportInfo {
if (entryPointAstModule.isExternal) {
throw new Error('fetchAstModuleExportInfo() is not supported for external modules');
}

if (entryPointAstModule.astModuleExportInfo === undefined) {
const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo();
const astModuleExportInfo: IAstModuleExportInfo = {
visitedAstModules: new Set<AstModule>(),
exportedLocalEntities: new Map<string, AstEntity>(),
starExportedExternalModules: new Set<AstModule>()
};

this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set<AstModule>());
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule);

entryPointAstModule.astModuleExportInfo = astModuleExportInfo;
}
Expand Down Expand Up @@ -314,18 +318,15 @@ export class ExportAnalyzer {
return this._importableAmbientSourceFiles.has(sourceFile);
}

private _collectAllExportsRecursive(
astModuleExportInfo: AstModuleExportInfo,
astModule: AstModule,
visitedAstModules: Set<AstModule>
): void {
private _collectAllExportsRecursive(astModuleExportInfo: IAstModuleExportInfo, astModule: AstModule): void {
const { visitedAstModules, starExportedExternalModules, exportedLocalEntities } = astModuleExportInfo;
if (visitedAstModules.has(astModule)) {
return;
}
visitedAstModules.add(astModule);

if (astModule.isExternal) {
astModuleExportInfo.starExportedExternalModules.add(astModule);
starExportedExternalModules.add(astModule);
} else {
// Fetch each of the explicit exports for this module
if (astModule.moduleSymbol.exports) {
Expand All @@ -337,7 +338,7 @@ export class ExportAnalyzer {
default:
// Don't collect the "export default" symbol unless this is the entry point module
if (exportName !== ts.InternalSymbolName.Default || visitedAstModules.size === 1) {
if (!astModuleExportInfo.exportedLocalEntities.has(exportSymbol.name)) {
if (!exportedLocalEntities.has(exportSymbol.name)) {
const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule);

if (astEntity instanceof AstSymbol && !astEntity.isExternal) {
Expand All @@ -348,7 +349,7 @@ export class ExportAnalyzer {
this._astSymbolTable.analyze(astEntity);
}

astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity);
exportedLocalEntities.set(exportSymbol.name, astEntity);
}
}
break;
Expand All @@ -357,7 +358,7 @@ export class ExportAnalyzer {
}

for (const starExportedModule of astModule.starExportedModules) {
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules);
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule);
}
}
}
Expand Down
106 changes: 84 additions & 22 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ExtractorMessageId } from '../api/ExtractorMessageId';
import { CollectorEntity } from './CollectorEntity';
import { AstSymbolTable } from '../analyzer/AstSymbolTable';
import type { AstEntity } from '../analyzer/AstEntity';
import type { AstModule, AstModuleExportInfo } from '../analyzer/AstModule';
import type { AstModule, IAstModuleExportInfo } from '../analyzer/AstModule';
import { AstSymbol } from '../analyzer/AstSymbol';
import type { AstDeclaration } from '../analyzer/AstDeclaration';
import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers';
Expand Down Expand Up @@ -313,12 +313,12 @@ export class Collector {
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}

const astModuleExportInfo: AstModuleExportInfo =
const { exportedLocalEntities, starExportedExternalModules, visitedAstModules }: IAstModuleExportInfo =
this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);

// Create a CollectorEntity for each top-level export.
const processedAstEntities: AstEntity[] = [];
for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) {
for (const [exportName, astEntity] of exportedLocalEntities) {
this._createCollectorEntity(astEntity, exportName);
processedAstEntities.push(astEntity);
}
Expand All @@ -333,9 +333,33 @@ export class Collector {
}
}

// Ensure references are collected from any intermediate files that
// only include exports
const nonExternalSourceFiles: Set<ts.SourceFile> = new Set();
for (const { sourceFile, isExternal } of visitedAstModules) {
if (!nonExternalSourceFiles.has(sourceFile) && !isExternal) {
nonExternalSourceFiles.add(sourceFile);
}
}

// Here, we're collecting reference directives from all non-external source files
// that were encountered while looking for exports, but only those references that
// were explicitly written by the developer and marked with the `preserve="true"`
// attribute. In TS >= 5.5, only references that are explicitly authored and marked
// with `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The `_collectReferenceDirectives` function pulls in all references in files that
// contain definitions, but does not examine files that only reexport from other
// files. Here, we're looking through files that were missed by `_collectReferenceDirectives`,
// but only collecting references that were explicitly marked with `preserve="true"`.
// It is intuitive for developers to include references that they explicitly want part of
// their public API in a file like the entrypoint, which is likely to only contain reexports,
// and this picks those up.
this._collectReferenceDirectivesFromSourceFiles(nonExternalSourceFiles, true);

this._makeUniqueNames();

for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) {
for (const starExportedExternalModule of starExportedExternalModules) {
if (starExportedExternalModule.externalModulePath !== undefined) {
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
}
Expand Down Expand Up @@ -539,7 +563,7 @@ export class Collector {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
const parentEntity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
if (!parentEntity) {
// This should never happen, as we've already created entities for all AstNamespaceImports.
Expand Down Expand Up @@ -992,44 +1016,82 @@ export class Collector {
}

private _collectReferenceDirectives(astEntity: AstEntity): void {
// Here, we're collecting reference directives from source files that contain extracted
// definitions (i.e. - files that contain `export class ...`, `export interface ...`, ...).
// These references may or may not include the `preserve="true" attribute. In TS < 5.5,
// references that end up in .D.TS files may or may not be explicity written by the developer.
// In TS >= 5.5, only references that are explicitly authored and are marked with
// `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The calls to `_collectReferenceDirectivesFromSourceFiles` in this function are
// preserving existing behavior, which is to include all reference directives
// regardless of whether they are explicitly authored or not, but only in files that
// contain definitions.

if (astEntity instanceof AstSymbol) {
const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) =>
astDeclaration.declaration.getSourceFile()
);
return this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
return this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
}

if (astEntity instanceof AstNamespaceImport) {
const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile];
return this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
return this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
}
}

private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void {
private _collectReferenceDirectivesFromSourceFiles(
sourceFiles: Iterable<ts.SourceFile>,
onlyIncludeExplicitlyPreserved: boolean
): void {
const seenFilenames: Set<string> = new Set<string>();

for (const sourceFile of sourceFiles) {
if (sourceFile && sourceFile.fileName) {
if (!seenFilenames.has(sourceFile.fileName)) {
seenFilenames.add(sourceFile.fileName);

for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) {
const name: string = sourceFile.text.substring(
typeReferenceDirective.pos,
typeReferenceDirective.end
if (sourceFile?.fileName) {
const {
fileName,
typeReferenceDirectives,
libReferenceDirectives,
text: sourceFileText
} = sourceFile;
if (!seenFilenames.has(fileName)) {
seenFilenames.add(fileName);

for (const typeReferenceDirective of typeReferenceDirectives) {
const name: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
typeReferenceDirective,
onlyIncludeExplicitlyPreserved
);
this._dtsTypeReferenceDirectives.add(name);
if (name) {
this._dtsTypeReferenceDirectives.add(name);
}
}

for (const libReferenceDirective of sourceFile.libReferenceDirectives) {
const name: string = sourceFile.text.substring(
libReferenceDirective.pos,
libReferenceDirective.end
for (const libReferenceDirective of libReferenceDirectives) {
const reference: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
libReferenceDirective,
onlyIncludeExplicitlyPreserved
);
this._dtsLibReferenceDirectives.add(name);
if (reference) {
this._dtsLibReferenceDirectives.add(reference);
}
}
}
}
}
}

private _getReferenceDirectiveFromSourceFile(
sourceFileText: string,
{ pos, end, preserve }: ts.FileReference,
onlyIncludeExplicitlyPreserved: boolean
): string | undefined {
const reference: string = sourceFileText.substring(pos, end);
if (preserve || !onlyIncludeExplicitlyPreserved) {
return reference;
}
}
}
4 changes: 2 additions & 2 deletions apps/api-extractor/src/enhancers/ValidationEnhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { CollectorEntity } from '../collector/CollectorEntity';
import { ExtractorMessageId } from '../api/ExtractorMessageId';
import { ReleaseTag } from '@microsoft/api-extractor-model';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';
import type { AstModuleExportInfo } from '../analyzer/AstModule';
import type { IAstModuleExportInfo } from '../analyzer/AstModule';
import type { AstEntity } from '../analyzer/AstEntity';

export class ValidationEnhancer {
Expand Down Expand Up @@ -47,7 +47,7 @@ export class ValidationEnhancer {
// A namespace created using "import * as ___ from ___"
const astNamespaceImport: AstNamespaceImport = entity.astEntity;

const astModuleExportInfo: AstModuleExportInfo =
const astModuleExportInfo: IAstModuleExportInfo =
astNamespaceImport.fetchAstModuleExportInfo(collector);

for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
Expand Down
4 changes: 2 additions & 2 deletions apps/api-extractor/src/generators/ApiReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IndentedWriter } from './IndentedWriter';
import { DtsEmitHelpers } from './DtsEmitHelpers';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';
import type { AstEntity } from '../analyzer/AstEntity';
import type { AstModuleExportInfo } from '../analyzer/AstModule';
import type { IAstModuleExportInfo } from '../analyzer/AstModule';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter';
import { ExtractorMessageId } from '../api/ExtractorMessageId';
import type { ApiReportVariant } from '../api/IConfigFile';
Expand Down Expand Up @@ -153,7 +153,7 @@ export class ApiReportGenerator {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);

if (entity.nameForEmit === undefined) {
// This should never happen
Expand Down
4 changes: 2 additions & 2 deletions apps/api-extractor/src/generators/DtsRollupGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { IndentedWriter } from './IndentedWriter';
import { DtsEmitHelpers } from './DtsEmitHelpers';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';
import type { AstModuleExportInfo } from '../analyzer/AstModule';
import type { IAstModuleExportInfo } from '../analyzer/AstModule';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter';
import type { AstEntity } from '../analyzer/AstEntity';

Expand Down Expand Up @@ -153,7 +153,7 @@ export class DtsRollupGenerator {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);

if (entity.nameForEmit === undefined) {
// This should never happen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @packageDocumentation
*/

/// <reference types="long" />

import { ISimpleInterface } from 'api-extractor-test-01';
import { ReexportedClass as RenamedReexportedClass3 } from 'api-extractor-test-01';
import * as semver1 from 'semver';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @packageDocumentation
*/

/// <reference types="long" />

import { ISimpleInterface } from 'api-extractor-test-01';
import { ReexportedClass as RenamedReexportedClass3 } from 'api-extractor-test-01';
import * as semver1 from 'semver';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @packageDocumentation
*/

/// <reference types="long" />

import { ISimpleInterface } from 'api-extractor-test-01';
import { ReexportedClass as RenamedReexportedClass3 } from 'api-extractor-test-01';
import * as semver1 from 'semver';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @packageDocumentation
*/

/// <reference types="long" />

import { ISimpleInterface } from 'api-extractor-test-01';
import { ReexportedClass as RenamedReexportedClass3 } from 'api-extractor-test-01';
import * as semver1 from 'semver';
Expand Down
Loading
Loading