Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
// 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