diff --git a/apps/api-extractor/.vscode/launch.json b/apps/api-extractor/.vscode/launch.json index 1b3bd492b71..e5d4063286e 100644 --- a/apps/api-extractor/.vscode/launch.json +++ b/apps/api-extractor/.vscode/launch.json @@ -11,9 +11,9 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-01", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true }, @@ -24,9 +24,9 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-02", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true }, @@ -37,9 +37,9 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-03", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true }, @@ -50,9 +50,9 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-04", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true }, @@ -63,22 +63,24 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-05", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true }, { "type": "node", "request": "launch", - "name": "test-06", + "name": "scenario", "program": "${workspaceFolder}/lib/start.js", - "cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-06", + "cwd": "${workspaceFolder}/../../build-tests/api-extractor-scenarios", "args": [ - "-d", + "--debug", "run", - "-l" + "--local", + "--config", + "./temp/configs/api-extractor-defaultExportOfEntryPoint1.json" ], "sourceMaps": true }, @@ -89,9 +91,9 @@ "program": "${workspaceFolder}/lib/start.js", "cwd": "(your project path)", "args": [ - "-d", + "--debug", "run", - "-l" + "--local" ], "sourceMaps": true } diff --git a/apps/api-extractor/src/analyzer/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts index 12f0bfbc8c6..1b13868ef7b 100644 --- a/apps/api-extractor/src/analyzer/AstDeclaration.ts +++ b/apps/api-extractor/src/analyzer/AstDeclaration.ts @@ -29,7 +29,7 @@ export interface IAstDeclarationOptions { * of analyzing AEDoc and emitting *.d.ts files. * * The AstDeclarations correspond to items from the compiler's ts.Node hierarchy, but - * omitting/skipping any nodes that don't match the SymbolAnalyzer.isAstDeclaration() + * omitting/skipping any nodes that don't match the AstDeclaration.isSupportedSyntaxKind() * criteria. This simplification makes the other API Extractor stages easier to implement. */ export class AstDeclaration { @@ -121,7 +121,7 @@ export class AstDeclaration { public getDump(indent: string = ''): string { const declarationKind: string = ts.SyntaxKind[this.declaration.kind]; let result: string = indent + `+ ${this.astSymbol.localName} (${declarationKind})`; - if (this.astSymbol.nominal) { + if (this.astSymbol.nominalAnalysis) { result += ' (nominal)'; } result += '\n'; @@ -180,4 +180,40 @@ export class AstDeclaration { child.forEachDeclarationRecursive(action); } } + + /** + * This function determines which ts.Node kinds will generate an AstDeclaration. + * These correspond to the definitions that we can add AEDoc to. + */ + public static isSupportedSyntaxKind(kind: ts.SyntaxKind): boolean { + // (alphabetical order) + switch (kind) { + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ConstructSignature: // Example: "new(x: number): IMyClass" + case ts.SyntaxKind.Constructor: // Example: "constructor(x: number)" + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.FunctionDeclaration: // Example: "(x: number): number" + case ts.SyntaxKind.IndexSignature: // Example: "[key: string]: string" + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.ModuleDeclaration: // Used for both "module" and "namespace" declarations + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.TypeAliasDeclaration: // Example: "type Shape = Circle | Square" + case ts.SyntaxKind.VariableDeclaration: + return true; + + // NOTE: In contexts where a source file is treated as a module, we do create "nominal analysis" + // AstSymbol objects corresponding to a ts.SyntaxKind.SourceFile node. However, a source file + // is NOT considered a nesting structure, and it does NOT act as a root for the declarations + // appearing in the file. This is because the *.d.ts generator is in the business of rolling up + // source files, and thus wants to ignore them in general. + } + + return false; + } + } diff --git a/apps/api-extractor/src/analyzer/AstEntryPoint.ts b/apps/api-extractor/src/analyzer/AstEntryPoint.ts deleted file mode 100644 index aa19c397e27..00000000000 --- a/apps/api-extractor/src/analyzer/AstEntryPoint.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { AstSymbol } from './AstSymbol'; - -/** - * Constructor options for AstEntryPoint - */ -export interface IExportedMember { - readonly name: string; - readonly astSymbol: AstSymbol; -} - -export interface IAstEntryPointOptions { - readonly exportedMembers: ReadonlyArray; -} - -/** - * This class is used by AstSymbolTable to return an entry point. - * (If AstDeclaration could be used to represent a ts.SyntaxKind.SourceFile node, - * then this class would not be needed.) - */ -export class AstEntryPoint { - public readonly exportedMembers: ReadonlyArray; - - public constructor(options: IAstEntryPointOptions) { - this.exportedMembers = options.exportedMembers; - } -} diff --git a/apps/api-extractor/src/analyzer/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts index 99511387957..fac7e40a1fc 100644 --- a/apps/api-extractor/src/analyzer/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -16,26 +16,24 @@ export interface IAstImportOptions { export class AstImport { /** * The name of the external package (and possibly module path) that this definition - * was imported from. If it was defined in the referencing source file, or if it was - * imported from a local file, or if it is an ambient definition, then externalPackageName - * will be undefined. + * was imported from. * - * Example: "@microsoft/gulp-core-build/lib/IBuildConfig" + * Example: "@microsoft/node-core-library/lib/FileSystem" */ public readonly modulePath: string; /** - * If importPackagePath is defined, then this specifies the export name for the definition. + * If modulePath is defined, then this specifies the export name for the definition. * * Example: "IBuildConfig" */ public readonly exportName: string; /** - * If importPackagePath and importPackageExportName are defined, then this is a dictionary key + * If modulePath and exportName are defined, then this is a dictionary key * that combines them with a colon (":"). * - * Example: "@microsoft/gulp-core-build/lib/IBuildConfig:IBuildConfig" + * Example: "@microsoft/node-core-library/lib/FileSystem:FileSystem" */ public readonly key: string; diff --git a/apps/api-extractor/src/analyzer/AstModule.ts b/apps/api-extractor/src/analyzer/AstModule.ts new file mode 100644 index 00000000000..511f0aace82 --- /dev/null +++ b/apps/api-extractor/src/analyzer/AstModule.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; + +import { AstSymbol } from './AstSymbol'; + +/** + */ +export class AstModule { + public readonly sourceFile: ts.SourceFile; + + public readonly exportedSymbols: Map; + public readonly starExportedExternalModules: Set; + + /** + * Example: "@microsoft/node-core-library/lib/FileSystem" + * but never: "./FileSystem" + */ + public externalModulePath: string | undefined; + + public constructor(sourceFile: ts.SourceFile) { + this.sourceFile = sourceFile; + this.exportedSymbols = new Map(); + this.starExportedExternalModules = new Set(); + this.externalModulePath = undefined; + } + + public get isExternal(): boolean { + return this.externalModulePath !== undefined; + } + +} diff --git a/apps/api-extractor/src/analyzer/AstSymbol.ts b/apps/api-extractor/src/analyzer/AstSymbol.ts index 61d564ff07c..5e85b8c981c 100644 --- a/apps/api-extractor/src/analyzer/AstSymbol.ts +++ b/apps/api-extractor/src/analyzer/AstSymbol.ts @@ -13,7 +13,7 @@ export interface IAstSymbolOptions { readonly followedSymbol: ts.Symbol; readonly localName: string; readonly astImport: AstImport | undefined; - readonly nominal: boolean; + readonly nominalAnalysis: boolean; readonly parentAstSymbol: AstSymbol | undefined; readonly rootAstSymbol: AstSymbol | undefined; } @@ -44,7 +44,9 @@ export class AstSymbol { /** * If this symbol was imported from another package, that information is tracked here. - * Otherwise, the value is undefined. + * Otherwise, the value is undefined. For example, if this symbol was defined in the referencing source file, + * or if it was imported from a local file in the current project, or if it is an ambient definition, + * then astImport will be undefined. */ public readonly astImport: AstImport | undefined; @@ -55,7 +57,7 @@ export class AstSymbol { * * Nominal symbols are tracked because we still need to emit exports for them. */ - public readonly nominal: boolean; + public readonly nominalAnalysis: boolean; /** * Returns the symbol of the parent of this AstSymbol, or undefined if there is no parent. @@ -90,7 +92,7 @@ export class AstSymbol { this.followedSymbol = options.followedSymbol; this.localName = options.localName; this.astImport = options.astImport; - this.nominal = options.nominal; + this.nominalAnalysis = options.nominalAnalysis; this.parentAstSymbol = options.parentAstSymbol; this.rootAstSymbol = options.rootAstSymbol || this; this._astDeclarations = []; diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 238400c9cdc..3a27f9b61d6 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -1,31 +1,33 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -/* tslint:disable:no-bitwise */ - import * as ts from 'typescript'; import { PackageJsonLookup, InternalError } from '@microsoft/node-core-library'; import { AstDeclaration } from './AstDeclaration'; -import { SymbolAnalyzer, IFollowAliasesResult } from './SymbolAnalyzer'; import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; -import { AstImport } from './AstImport'; -import { AstEntryPoint, IExportedMember } from './AstEntryPoint'; +import { AstImport, IAstImportOptions } from './AstImport'; +import { AstModule } from './AstModule'; import { PackageMetadataManager } from './PackageMetadataManager'; import { ILogger } from '../api/ILogger'; +import { ExportAnalyzer } from './ExportAnalyzer'; /** * AstSymbolTable is the workhorse that builds AstSymbol and AstDeclaration objects. * It maintains a cache of already constructed objects. AstSymbolTable constructs - * AstEntryPoint objects, but otherwise the state that it maintains is agnostic of + * AstModule objects, but otherwise the state that it maintains is agnostic of * any particular entry point. (For example, it does not track whether a given AstSymbol * is "exported" or not.) + * + * Internally, AstSymbolTable relies on ExportAnalyzer to crawl import statements and determine where symbols + * are declared (i.e. the AstImport information needed to import them). */ export class AstSymbolTable { private readonly _program: ts.Program; private readonly _typeChecker: ts.TypeChecker; private readonly _packageMetadataManager: PackageMetadataManager; + private readonly _exportAnalyzer: ExportAnalyzer; /** * A mapping from ts.Symbol --> AstSymbol @@ -49,53 +51,33 @@ export class AstSymbolTable { */ private readonly _astSymbolsByImportKey: Map = new Map(); - /** - * Cache of fetchEntryPoint() results. - */ - private readonly _astEntryPointsBySourceFile: Map - = new Map(); - public constructor(program: ts.Program, typeChecker: ts.TypeChecker, packageJsonLookup: PackageJsonLookup, logger: ILogger) { this._program = program; this._typeChecker = typeChecker; this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, logger); + + this._exportAnalyzer = new ExportAnalyzer( + this._program, + this._typeChecker, + { + analyze: this.analyze.bind(this), + fetchAstSymbol: this._fetchAstSymbol.bind(this) + } + ); } /** - * For a given source file, this analyzes all of its exports and produces an AstEntryPoint + * For a given source file, this analyzes all of its exports and produces an AstModule * object. */ - public fetchEntryPoint(sourceFile: ts.SourceFile): AstEntryPoint { - let astEntryPoint: AstEntryPoint | undefined = this._astEntryPointsBySourceFile.get(sourceFile); - if (!astEntryPoint) { - const rootFileSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(sourceFile); - - if (!rootFileSymbol.declarations || !rootFileSymbol.declarations.length) { - throw new Error('Unable to find a root declaration for ' + sourceFile.fileName); - } - - const exportSymbols: ts.Symbol[] = this._typeChecker.getExportsOfModule(rootFileSymbol) || []; - - const exportedMembers: IExportedMember[] = []; - - for (const exportSymbol of exportSymbols) { - const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(exportSymbol, true); - - if (!astSymbol) { - throw new Error('Unsupported export: ' + exportSymbol.name); - } - - this.analyze(astSymbol); - - exportedMembers.push({ name: exportSymbol.name, astSymbol: astSymbol }); - } + public fetchEntryPointModule(sourceFile: ts.SourceFile): AstModule { + return this._exportAnalyzer.fetchAstModuleBySourceFile(sourceFile, undefined); + } - astEntryPoint = new AstEntryPoint({ exportedMembers }); - this._astEntryPointsBySourceFile.set(sourceFile, astEntryPoint); - } - return astEntryPoint; + public fetchReferencedAstSymbol(symbol: ts.Symbol, sourceFile: ts.SourceFile): AstSymbol | undefined { + return this._exportAnalyzer.fetchReferencedAstSymbol(symbol, sourceFile); } /** @@ -103,9 +85,10 @@ export class AstSymbolTable { * starts from the root symbol and then fills out all children of all declarations, and * also calculates AstDeclaration.referencedAstSymbols for all declarations. * If the symbol is not imported, any non-imported references are also analyzed. + * * @remarks * This is an expensive operation, so we only perform it for top-level exports of an - * the AstEntryPoint. For example, if some code references a nested class inside + * the AstModule. For example, if some code references a nested class inside * a namespace from another library, we do not analyze any of that class's siblings * or members. (We do always construct its parents however, since AstDefinition.parent * is immutable, and needed e.g. to calculate release tag inheritance.) @@ -115,7 +98,7 @@ export class AstSymbolTable { return; } - if (astSymbol.nominal) { + if (astSymbol.nominalAnalysis) { // We don't analyze nominal symbols astSymbol._notifyAnalyzed(); return; @@ -151,7 +134,7 @@ export class AstSymbolTable { * This will not analyze or construct any new AstSymbol objects. */ public tryGetAstSymbol(symbol: ts.Symbol): AstSymbol | undefined { - return this._fetchAstSymbol(symbol, false); + return this._fetchAstSymbol(symbol, false, undefined); } /** @@ -204,7 +187,8 @@ export class AstSymbolTable { throw new Error('Symbol not found for identifier: ' + symbolNode.getText()); } - const referencedAstSymbol: AstSymbol | undefined = this._fetchAstSymbol(symbol, true); + const referencedAstSymbol: AstSymbol | undefined + = this.fetchReferencedAstSymbol(symbol, symbolNode.getSourceFile()); if (referencedAstSymbol) { governingAstDeclaration._notifyReferencedAstSymbol(referencedAstSymbol); } @@ -237,7 +221,7 @@ export class AstSymbolTable { } private _fetchAstSymbolForNode(node: ts.Node): AstSymbol | undefined { - if (!SymbolAnalyzer.isAstDeclaration(node.kind)) { + if (!AstDeclaration.isSupportedSyntaxKind(node.kind)) { return undefined; } @@ -246,20 +230,20 @@ export class AstSymbolTable { throw new InternalError('Unable to find symbol for node'); } - return this._fetchAstSymbol(symbol, true); + return this._fetchAstSymbol(symbol, true, undefined); } - private _fetchAstSymbol(symbol: ts.Symbol, addIfMissing: boolean): AstSymbol | undefined { - const followAliasesResult: IFollowAliasesResult = SymbolAnalyzer.followAliases(symbol, this._typeChecker); - - const followedSymbol: ts.Symbol = followAliasesResult.followedSymbol; + private _fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, + astImportOptions: IAstImportOptions | undefined, localName?: string): AstSymbol | undefined { // Filter out symbols representing constructs that we don't care about + // tslint:disable-next-line:no-bitwise if (followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient)) { return undefined; } - if (followAliasesResult.isAmbient) { + if (TypeScriptHelpers.isAmbient(followedSymbol, this._typeChecker)) { + // API Extractor doesn't analyze ambient declarations at all return undefined; } @@ -270,7 +254,7 @@ export class AstSymbolTable { throw new InternalError('Followed a symbol with no declarations'); } - const astImport: AstImport | undefined = followAliasesResult.astImport; + const astImport: AstImport | undefined = astImportOptions ? new AstImport(astImportOptions) : undefined; if (astImport) { if (!astSymbol) { @@ -285,7 +269,7 @@ export class AstSymbolTable { if (!astSymbol) { // None of the above lookups worked, so create a new entry... - let nominal: boolean = false; + let nominalAnalysis: boolean = false; // NOTE: In certain circumstances we need an AstSymbol for a source file that is acting // as a TypeScript module. For example, one of the unit tests has this line: @@ -299,23 +283,23 @@ export class AstSymbolTable { // false, we do create an AstDeclaration for a ts.SyntaxKind.SourceFile in this special edge case. if (followedSymbol.declarations.length === 1 && followedSymbol.declarations[0].kind === ts.SyntaxKind.SourceFile) { - nominal = true; + nominalAnalysis = true; } // If the file is from a package that does not support AEDoc, then we process the // symbol itself, but we don't attempt to process any parent/children of it. const followedSymbolSourceFile: ts.SourceFile = followedSymbol.declarations[0].getSourceFile(); - if (this._program.isSourceFileFromExternalLibrary(followedSymbolSourceFile)) { + if (astImport !== undefined) { if (!this._packageMetadataManager.isAedocSupportedFor(followedSymbolSourceFile.fileName)) { - nominal = true; + nominalAnalysis = true; } } let parentAstSymbol: AstSymbol | undefined = undefined; - if (!nominal) { + if (!nominalAnalysis) { for (const declaration of followedSymbol.declarations || []) { - if (!SymbolAnalyzer.isAstDeclaration(declaration.kind)) { + if (!AstDeclaration.isSupportedSyntaxKind(declaration.kind)) { throw new InternalError(`The "${followedSymbol.name}" symbol uses the construct` + ` "${ts.SyntaxKind[declaration.kind]}" which may be an unimplemented language feature`); } @@ -340,7 +324,7 @@ export class AstSymbolTable { const parentSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration( arbitaryParentDeclaration as ts.Declaration); - parentAstSymbol = this._fetchAstSymbol(parentSymbol, addIfMissing); + parentAstSymbol = this._fetchAstSymbol(parentSymbol, addIfMissing, undefined); if (!parentAstSymbol) { throw new InternalError('Unable to construct a parent AstSymbol for ' + followedSymbol.name); @@ -348,13 +332,27 @@ export class AstSymbolTable { } } + if (localName === undefined) { + // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name + // This handles cases such as "export default class X { }" where the symbol name is "default" + // but the declaration name is "X". + localName = followedSymbol.name; + for (const declaration of followedSymbol.declarations || []) { + const declarationNameIdentifier: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration); + if (declarationNameIdentifier && ts.isIdentifier(declarationNameIdentifier)) { + localName = declarationNameIdentifier.getText().trim(); + break; + } + } + } + astSymbol = new AstSymbol({ - localName: followAliasesResult.localName, - followedSymbol: followAliasesResult.followedSymbol, + localName: localName, + followedSymbol: followedSymbol, astImport: astImport, parentAstSymbol: parentAstSymbol, rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined, - nominal: nominal + nominalAnalysis: nominalAnalysis }); this._astSymbolsBySymbol.set(followedSymbol, astSymbol); @@ -390,7 +388,7 @@ export class AstSymbolTable { } } - if (followAliasesResult.astImport && !astSymbol.imported) { + if (astImportOptions && !astSymbol.imported) { // Our strategy for recognizing external declarations is to look for an import statement // during SymbolAnalyzer.followAliases(). Although it is sometimes possible to reach a symbol // without traversing an import statement, we assume that that the first reference will always @@ -411,7 +409,7 @@ export class AstSymbolTable { private _tryFindFirstAstDeclarationParent(node: ts.Node): ts.Node | undefined { let currentNode: ts.Node | undefined = node.parent; while (currentNode) { - if (SymbolAnalyzer.isAstDeclaration(currentNode.kind)) { + if (AstDeclaration.isSupportedSyntaxKind(currentNode.kind)) { return currentNode; } currentNode = currentNode.parent; diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts new file mode 100644 index 00000000000..abbe7f31017 --- /dev/null +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -0,0 +1,417 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import { InternalError } from '@microsoft/node-core-library'; + +import { TypeScriptHelpers } from './TypeScriptHelpers'; +import { AstSymbol } from './AstSymbol'; +import { IAstImportOptions } from './AstImport'; +import { AstModule } from './AstModule'; +import { TypeScriptInternals } from './TypeScriptInternals'; + +/** + * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. + * + * In particular, we want ExportAnalyzer to be able to call AstSymbolTable._fetchAstSymbol() even though it + * is a very private API that should not be exposed to any other components. + */ +export interface IAstSymbolTable { + fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, + astImportOptions: IAstImportOptions | undefined, localName?: string): AstSymbol | undefined; + + analyze(astSymbol: AstSymbol): void; +} + +/** + * The ExportAnalyzer is an internal part of AstSymbolTable that has been moved out into its own source file + * because it is a complex and mostly self-contained algorithm. + * + * Its job is to build up AstModule objects by crawling import statements to discover where declarations come from. + * This is conceptually the same as the compiler's own TypeChecker.getExportsOfModule(), except that when + * ExportAnalyzer encounters a declaration that was imported from an external package, it remembers how it was imported + * (i.e. the AstImport object). Today the compiler API does not expose this information, which is crucial for + * generating .d.ts rollups. + */ +export class ExportAnalyzer { + private readonly _program: ts.Program; + private readonly _typeChecker: ts.TypeChecker; + private readonly _astSymbolTable: IAstSymbolTable; + + private readonly _astModulesBySourceFile: Map + = new Map(); + + public constructor(program: ts.Program, typeChecker: ts.TypeChecker, astSymbolTable: IAstSymbolTable) { + this._program = program; + this._typeChecker = typeChecker; + this._astSymbolTable = astSymbolTable; + } + + /** + * For a given source file, this analyzes all of its exports and produces an AstModule object. + */ + public fetchAstModuleBySourceFile(sourceFile: ts.SourceFile, moduleSpecifier: string | undefined): AstModule { + // Don't traverse into a module that we already processed before: + // The compiler allows m1 to have "export * from 'm2'" and "export * from 'm3'", + // even if m2 and m3 both have "export * from 'm4'". + let astModule: AstModule | undefined = this._astModulesBySourceFile.get(sourceFile); + + if (!astModule) { + astModule = new AstModule(sourceFile); + this._astModulesBySourceFile.set(sourceFile, astModule); + + const moduleSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(sourceFile); + + // Match: "@microsoft/sp-lodash-subset" or "lodash/has" + // but ignore: "../folder/LocalFile" + // + // (For the entry point of the local project being analyzed, moduleSpecifier === undefined) + if (moduleSpecifier !== undefined && !ts.isExternalModuleNameRelative(moduleSpecifier)) { + // This makes astModule.isExternal=true + astModule.externalModulePath = moduleSpecifier; + } + + if (astModule.isExternal) { + // It's an external package, so do the special simplified analysis that doesn't crawl into referenced modules + astModule.externalModulePath = moduleSpecifier; + + for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) { + + const astImportOptions: IAstImportOptions = { + exportName: exportedSymbol.name, + modulePath: moduleSpecifier! + }; + + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol( + followedSymbol, true, astImportOptions); + + if (!astSymbol) { + throw new Error('Unsupported export: ' + exportedSymbol.name); + } + + astModule.exportedSymbols.set(exportedSymbol.name, astSymbol); + } + } else { + // The module is part of the local project, so do the full analysis + + if (moduleSymbol.exports) { + for (const exportedSymbol of moduleSymbol.exports.values() as IterableIterator) { + + if (exportedSymbol.escapedName === ts.InternalSymbolName.ExportStar) { + // Special handling for "export * from 'module-name';" declarations, which are all attached to a single + // symbol whose name is InternalSymbolName.ExportStar + for (const exportStarDeclaration of exportedSymbol.getDeclarations() || []) { + this._collectExportsFromExportStar(astModule, exportStarDeclaration); + } + + } else { + const fetchedAstSymbol: AstSymbol | undefined = this._fetchAstSymbolFromModule(astModule, exportedSymbol); + if (fetchedAstSymbol !== undefined) { + astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); + } + } + } + + } + + } + + if (!astModule.isExternal) { + for (const exportedAstSymbol of astModule.exportedSymbols.values()) { + this._astSymbolTable.analyze(exportedAstSymbol); + } + } + } + + return astModule; + } + + /** + * For a given symbol (which was encountered in the specified sourceFile), this fetches the AstSymbol that it + * refers to. For example, if a particular interface describes the return value of a function, this API can help + * us determine a TSDoc declaration reference for that symbol (if the symbol is exported). + */ + public fetchReferencedAstSymbol(symbol: ts.Symbol, sourceFile: ts.SourceFile): AstSymbol | undefined { + const astModule: AstModule | undefined = this._astModulesBySourceFile.get(sourceFile); + if (astModule === undefined) { + throw new InternalError('fetchReferencedAstSymbol() called for a source file that was not analyzed'); + } + + return this._fetchAstSymbolFromModule(astModule, symbol); + } + + private _fetchAstSymbolFromModule(astModule: AstModule, symbol: ts.Symbol): AstSymbol | undefined { + let current: ts.Symbol = symbol; + + while (true) { // tslint:disable-line:no-constant-condition + + // Is this symbol an import/export that we need to follow to find the real declaration? + for (const declaration of current.declarations || []) { + let matchedAstSymbol: AstSymbol | undefined; + matchedAstSymbol = this._tryMatchExportDeclaration(astModule, symbol, declaration); + if (matchedAstSymbol !== undefined) { + return matchedAstSymbol; + } + matchedAstSymbol = this._tryMatchImportDeclaration(astModule, symbol, declaration); + if (matchedAstSymbol !== undefined) { + return matchedAstSymbol; + } + } + + if (!(current.flags & ts.SymbolFlags.Alias)) { // tslint:disable-line:no-bitwise + break; + } + + const currentAlias: ts.Symbol = TypeScriptInternals.getImmediateAliasedSymbol(current, this._typeChecker); + // Stop if we reach the end of the chain + if (!currentAlias || currentAlias === current) { + break; + } + + current = currentAlias; + } + + // Otherwise, assume it is a normal declaration + return this._astSymbolTable.fetchAstSymbol(current, true, undefined); + } + + private _tryMatchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + declaration: ts.Declaration): AstSymbol | undefined { + + const exportDeclaration: ts.ExportDeclaration | undefined + = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ExportDeclaration); + + if (exportDeclaration) { + let exportName: string | undefined = undefined; + + if (declaration.kind === ts.SyntaxKind.ExportSpecifier) { + // EXAMPLE: + // "export { A } from './file-a';" + // + // ExportDeclaration: + // ExportKeyword: pre=[export] sep=[ ] + // NamedExports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ExportSpecifier: <------------- declaration + // Identifier: pre=[A] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['./file-a'] + // SemicolonToken: pre=[;] + + // Example: " ExportName as RenamedName" + const exportSpecifier: ts.ExportSpecifier = declaration as ts.ExportSpecifier; + exportName = (exportSpecifier.propertyName || exportSpecifier.name).getText().trim(); + } else { + throw new InternalError('Unimplemented export declaration kind: ' + declaration.getText()); + } + + // Ignore "export { A }" without a module specifier + if (exportDeclaration.moduleSpecifier) { + const specifierAstModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration); + const astSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); + return astSymbol; + } + } + + return undefined; + } + + private _tryMatchImportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + declaration: ts.Declaration): AstSymbol | undefined { + + const importDeclaration: ts.ImportDeclaration | undefined + = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ImportDeclaration); + + if (importDeclaration) { + const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importDeclaration); + + if (declaration.kind === ts.SyntaxKind.NamespaceImport) { + // EXAMPLE: + // "import * as theLib from 'the-lib';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: + // NamespaceImport: <------------- declaration + // AsteriskToken: pre=[*] sep=[ ] + // AsKeyword: pre=[as] sep=[ ] + // Identifier: pre=[theLib] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + if (specifierAstModule.externalModulePath === undefined) { + // The implementation here only works when importing from an external module. + // The full solution is tracked by: https://github.com/Microsoft/web-build-tools/issues/1029 + throw new Error('"import * as ___ from ___;" is not supported yet for local files.' + + '\nFailure in: ' + importDeclaration.getSourceFile().fileName); + } + + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); + + const astImportOptions: IAstImportOptions = { + exportName: '*', + modulePath: specifierAstModule.externalModulePath + }; + + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol(followedSymbol, true, + astImportOptions, exportedSymbol.name); + return astSymbol; + } + + if (declaration.kind === ts.SyntaxKind.ImportSpecifier) { + // EXAMPLE: + // "import { A, B } from 'the-lib';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: + // NamedImports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ImportSpecifier: <------------- declaration + // Identifier: pre=[A] + // CommaToken: pre=[,] sep=[ ] + // ImportSpecifier: + // Identifier: pre=[B] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + // Example: " ExportName as RenamedName" + const importSpecifier: ts.ImportSpecifier = declaration as ts.ImportSpecifier; + const exportName: string = (importSpecifier.propertyName || importSpecifier.name).getText().trim(); + const astSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); + return astSymbol; + } else if (declaration.kind === ts.SyntaxKind.ImportClause) { + // EXAMPLE: + // "import A, { B } from './A';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: <------------- declaration (referring to A) + // Identifier: pre=[A] + // CommaToken: pre=[,] sep=[ ] + // NamedImports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ImportSpecifier: + // Identifier: pre=[B] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['./A'] + // SemicolonToken: pre=[;] + const astSymbol: AstSymbol = this._getExportOfAstModule(ts.InternalSymbolName.Default, specifierAstModule); + return astSymbol; + } else { + throw new InternalError('Unimplemented import declaration kind: ' + declaration.getText()); + } + } + + return undefined; + } + + private _getExportOfAstModule(exportName: string, astModule: AstModule): AstSymbol { + const visitedAstModules: Set = new Set(); + const astSymbol: AstSymbol | undefined = this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules); + if (astSymbol === undefined) { + throw new InternalError(`Unable to analyze the export ${JSON.stringify(exportName)}`); + } + return astSymbol; + } + + private _tryGetExportOfAstModule(exportName: string, astModule: AstModule, + visitedAstModules: Set): AstSymbol | undefined { + + if (visitedAstModules.has(astModule)) { + return undefined; + } + visitedAstModules.add(astModule); + + let astSymbol: AstSymbol | undefined = astModule.exportedSymbols.get(exportName); + if (astSymbol !== undefined) { + return astSymbol; + } + + // Try each of the star imports + for (const starExportedModule of astModule.starExportedExternalModules) { + astSymbol = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); + if (astSymbol !== undefined) { + return astSymbol; + } + } + + return undefined; + } + + /** + * Given an ImportDeclaration of the form `export * from "___";`, this copies all the exported declarations + * from the source module into the target AstModule. If the source module is an external package, + * it is simply added to targetModule.starExportedExternalModules. If the source module is a local file, + * then all of its contents are copied over. + */ + private _collectExportsFromExportStar(targetAstModule: AstModule, exportStarDeclaration: ts.Declaration): void { + if (ts.isExportDeclaration(exportStarDeclaration)) { + + const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(exportStarDeclaration); + if (starExportedModule !== undefined) { + if (starExportedModule.isExternal) { + targetAstModule.starExportedExternalModules.add(starExportedModule); + } else { + // Copy exportedSymbols from the other module + for (const [exportName, exportedSymbol] of starExportedModule.exportedSymbols) { + if (!targetAstModule.exportedSymbols.has(exportName)) { + targetAstModule.exportedSymbols.set(exportName, exportedSymbol); + } + } + // Copy starExportedExternalModules from the other module + for (const starExportedExternalModule of starExportedModule.starExportedExternalModules) { + targetAstModule.starExportedExternalModules.add(starExportedExternalModule); + } + } + } + + } else { + // Ignore ExportDeclaration nodes that don't match the expected pattern + // TODO: Should we report a warning? + } + } + + /** + * Given an ImportDeclaration of the form `export * from "___";`, this interprets the module specifier (`"___"`) + * and fetches the corresponding AstModule object. + */ + private _fetchSpecifierAstModule(exportStarDeclaration: ts.ImportDeclaration | ts.ExportDeclaration): AstModule { + + // The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point' + const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(exportStarDeclaration); + if (!moduleSpecifier) { + throw new InternalError('Unable to parse module specifier'); + } + + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( + exportStarDeclaration.getSourceFile(), moduleSpecifier); + + if (resolvedModule === undefined) { + // This should not happen, since getResolvedModule() specifically looks up names that the compiler + // found in export declarations for this source file + throw new InternalError('getResolvedModule() could not resolve module name ' + JSON.stringify(moduleSpecifier)); + } + + // Map the filename back to the corresponding SourceFile. This circuitous approach is needed because + // we have no way to access the compiler's internal resolveExternalModuleName() function + const moduleSourceFile: ts.SourceFile | undefined = this._program.getSourceFile(resolvedModule.resolvedFileName); + if (!moduleSourceFile) { + // This should not happen, since getResolvedModule() specifically looks up names that the compiler + // found in export declarations for this source file + throw new InternalError('getSourceFile() failed to locate ' + JSON.stringify(resolvedModule.resolvedFileName)); + } + + const specifierAstModule: AstModule = this.fetchAstModuleBySourceFile(moduleSourceFile, moduleSpecifier); + return specifierAstModule; + } +} diff --git a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts deleted file mode 100644 index 6bacaccc401..00000000000 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/* tslint:disable:no-bitwise */ - -import * as ts from 'typescript'; - -import { TypeScriptHelpers } from './TypeScriptHelpers'; -import { AstImport } from './AstImport'; - -/** - * Return value for DtsRollupGenerator._followAliases() - */ -export interface IFollowAliasesResult { - /** - * The original symbol that defined this entry, after following any aliases. - */ - readonly followedSymbol: ts.Symbol; - - /** - * The original name used where it was defined. - */ - readonly localName: string; - - /** - * True if this is an ambient definition, e.g. from a "typings" folder. - */ - readonly isAmbient: boolean; - - /** - * If this followedSymbol was reached by traversing - */ - readonly astImport: AstImport | undefined; -} - -/** - * This is a helper class for DtsRollupGenerator and AstSymbolTable. - * Its main role is to provide an expanded version of TypeScriptHelpers.followAliases() - * that supports tracking of imports from eternal packages. - */ -export class SymbolAnalyzer { - - /** - * This function determines which ts.Node kinds will generate an AstDeclaration. - * These correspond to the definitions that we can add AEDoc to. - */ - public static isAstDeclaration(kind: ts.SyntaxKind): boolean { - // (alphabetical order) - switch (kind) { - case ts.SyntaxKind.CallSignature: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.ConstructSignature: // Example: "new(x: number): IMyClass" - case ts.SyntaxKind.Constructor: // Example: "constructor(x: number)" - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.EnumMember: - case ts.SyntaxKind.FunctionDeclaration: // Example: "(x: number): number" - case ts.SyntaxKind.IndexSignature: // Example: "[key: string]: string" - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - case ts.SyntaxKind.ModuleDeclaration: // Used for both "module" and "namespace" declarations - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.PropertySignature: - case ts.SyntaxKind.TypeAliasDeclaration: // Example: "type Shape = Circle | Square" - case ts.SyntaxKind.VariableDeclaration: - return true; - - // NOTE: In contexts where a source file is treated as a module, we do create "nominal" - // AstSymbol objects corresponding to a ts.SyntaxKind.SourceFile node. However, a source file - // is NOT considered a nesting structure, and it does NOT act as a root for the declarations - // appearing in the file. This is because the *.d.ts generator is in the business of rolling up - // source files, and thus wants to ignore them in general. - } - - return false; - } - - /** - * For the given symbol, follow imports and type alias to find the symbol that represents - * the original definition. - */ - public static followAliases(symbol: ts.Symbol, typeChecker: ts.TypeChecker): IFollowAliasesResult { - let current: ts.Symbol = symbol; - - // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name - let declarationName: string | undefined = undefined; - - while (true) { // tslint:disable-line:no-constant-condition - for (const declaration of current.declarations || []) { - const declarationNameIdentifier: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration); - if (declarationNameIdentifier && ts.isIdentifier(declarationNameIdentifier)) { - declarationName = declarationNameIdentifier.getText().trim(); - } - - // 2. Check for any signs that this was imported from an external package - let result: IFollowAliasesResult | undefined; - - result = SymbolAnalyzer._followAliasesForExportDeclaration(declaration, current, typeChecker); - if (result) { - return result; - } - - result = SymbolAnalyzer._followAliasesForImportDeclaration(declaration, current, typeChecker); - if (result) { - return result; - } - } - - if (!(current.flags & ts.SymbolFlags.Alias)) { - break; - } - - const currentAlias: ts.Symbol = TypeScriptHelpers.getImmediateAliasedSymbol(current, typeChecker); - // Stop if we reach the end of the chain - if (!currentAlias || currentAlias === current) { - break; - } - - current = currentAlias; - } - - // Is this an ambient declaration? - let isAmbient: boolean = true; - if (current.declarations) { - - // Test 1: Are we inside the sinister "declare global {" construct? - let insideDeclareGlobal: boolean = false; - const highestModuleDeclaration: ts.ModuleDeclaration | undefined - = TypeScriptHelpers.findHighestParent(current.declarations[0], ts.SyntaxKind.ModuleDeclaration); - if (highestModuleDeclaration) { - if (highestModuleDeclaration.name.getText().trim() === 'global') { - insideDeclareGlobal = true; - } - } - - // Test 2: Otherwise, the main heuristic for ambient declarations is by looking at the - // ts.SyntaxKind.SourceFile node to see whether it has a symbol or not (i.e. whether it - // is acting as a module or not). - if (!insideDeclareGlobal) { - const sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( - current.declarations[0], ts.SyntaxKind.SourceFile); - if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { - isAmbient = false; - } - } - } - - return { - followedSymbol: current, - localName: declarationName || current.name, - astImport: undefined, - isAmbient: isAmbient - }; - } - - /** - * Helper function for _followAliases(), for handling ts.ExportDeclaration patterns - */ - private static _followAliasesForExportDeclaration(declaration: ts.Declaration, - symbol: ts.Symbol, typeChecker: ts.TypeChecker): IFollowAliasesResult | undefined { - - const exportDeclaration: ts.ExportDeclaration | undefined - = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ExportDeclaration); - - if (exportDeclaration) { - let exportName: string; - - if (declaration.kind === ts.SyntaxKind.ExportSpecifier) { - // EXAMPLE: - // "export { A } from './file-a';" - // - // ExportDeclaration: - // ExportKeyword: pre=[export] sep=[ ] - // NamedExports: - // FirstPunctuation: pre=[{] sep=[ ] - // SyntaxList: - // ExportSpecifier: <------------- declaration - // Identifier: pre=[A] sep=[ ] - // CloseBraceToken: pre=[}] sep=[ ] - // FromKeyword: pre=[from] sep=[ ] - // StringLiteral: pre=['./file-a'] - // SemicolonToken: pre=[;] - - // Example: " ExportName as RenamedName" - const exportSpecifier: ts.ExportSpecifier = declaration as ts.ExportSpecifier; - exportName = (exportSpecifier.propertyName || exportSpecifier.name).getText().trim(); - } else { - throw new Error('Unimplemented export declaration kind: ' + declaration.getText()); - } - - if (exportDeclaration.moduleSpecifier) { - // Examples: - // " '@microsoft/sp-lodash-subset'" - // " "lodash/has"" - const modulePath: string | undefined = SymbolAnalyzer._getPackagePathFromModuleSpecifier( - exportDeclaration.moduleSpecifier); - - if (modulePath) { - return { - followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), - localName: exportName, - astImport: new AstImport({ modulePath, exportName }), - isAmbient: false - }; - } - } - - } - - return undefined; - } - - /** - * Helper function for _followAliases(), for handling ts.ImportDeclaration patterns - */ - private static _followAliasesForImportDeclaration(declaration: ts.Declaration, - symbol: ts.Symbol, typeChecker: ts.TypeChecker): IFollowAliasesResult | undefined { - - const importDeclaration: ts.ImportDeclaration | undefined - = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ImportDeclaration); - - if (importDeclaration) { - let exportName: string; - - if (declaration.kind === ts.SyntaxKind.ImportSpecifier) { - // EXAMPLE: - // "import { A, B } from 'the-lib';" - // - // ImportDeclaration: - // ImportKeyword: pre=[import] sep=[ ] - // ImportClause: - // NamedImports: - // FirstPunctuation: pre=[{] sep=[ ] - // SyntaxList: - // ImportSpecifier: <------------- declaration - // Identifier: pre=[A] - // CommaToken: pre=[,] sep=[ ] - // ImportSpecifier: - // Identifier: pre=[B] sep=[ ] - // CloseBraceToken: pre=[}] sep=[ ] - // FromKeyword: pre=[from] sep=[ ] - // StringLiteral: pre=['the-lib'] - // SemicolonToken: pre=[;] - - // Example: " ExportName as RenamedName" - const importSpecifier: ts.ImportSpecifier = declaration as ts.ImportSpecifier; - exportName = (importSpecifier.propertyName || importSpecifier.name).getText().trim(); - } else if (declaration.kind === ts.SyntaxKind.NamespaceImport) { - // EXAMPLE: - // "import * as theLib from 'the-lib';" - // - // ImportDeclaration: - // ImportKeyword: pre=[import] sep=[ ] - // ImportClause: - // NamespaceImport: <------------- declaration - // AsteriskToken: pre=[*] sep=[ ] - // AsKeyword: pre=[as] sep=[ ] - // Identifier: pre=[theLib] sep=[ ] - // FromKeyword: pre=[from] sep=[ ] - // StringLiteral: pre=['the-lib'] - // SemicolonToken: pre=[;] - exportName = '*'; - } else if (declaration.kind === ts.SyntaxKind.ImportClause) { - // EXAMPLE: - // "import A, { B } from './A';" - // - // ImportDeclaration: - // ImportKeyword: pre=[import] sep=[ ] - // ImportClause: <------------- declaration (referring to A) - // Identifier: pre=[A] - // CommaToken: pre=[,] sep=[ ] - // NamedImports: - // FirstPunctuation: pre=[{] sep=[ ] - // SyntaxList: - // ImportSpecifier: - // Identifier: pre=[B] sep=[ ] - // CloseBraceToken: pre=[}] sep=[ ] - // FromKeyword: pre=[from] sep=[ ] - // StringLiteral: pre=['./A'] - // SemicolonToken: pre=[;] - exportName = ts.InternalSymbolName.Default; - } else { - throw new Error('Unimplemented import declaration kind: ' + declaration.getText()); - } - - if (importDeclaration.moduleSpecifier) { - // Examples: - // " '@microsoft/sp-lodash-subset'" - // " "lodash/has"" - const modulePath: string | undefined = SymbolAnalyzer._getPackagePathFromModuleSpecifier( - importDeclaration.moduleSpecifier); - - if (modulePath) { - return { - followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), - localName: symbol.name, - astImport: new AstImport({ modulePath, exportName }), - isAmbient: false - }; - } - } - - } - - return undefined; - } - - private static _getPackagePathFromModuleSpecifier(moduleSpecifier: ts.Expression): string | undefined { - // Examples: - // " '@microsoft/sp-lodash-subset'" - // " "lodash/has"" - // " './MyClass'" - const moduleSpecifierText: string = moduleSpecifier.getFullText(); - - // Remove quotes/whitespace - const path: string = moduleSpecifierText - .replace(/^\s*['"]/, '') - .replace(/['"]\s*$/, ''); - - // Does it start with something like "./" or "../"? - // Or is it a fixed string like "." or ".."? - if (/^\.\.?(\/|$)/.test(path)) { - // Yes, so there is no module specifier - return undefined; - } else { - // No, so we can assume it's an import from an external package - return path; - } - } -} diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index 6c2d381a940..29d8710a86f 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -5,10 +5,11 @@ import * as ts from 'typescript'; import { TypeScriptMessageFormatter } from './TypeScriptMessageFormatter'; +import { TypeScriptInternals } from './TypeScriptInternals'; export class TypeScriptHelpers { /** - * This traverses any type aliases to find the original place where an item was defined. + * This traverses any symbol aliases to find the original place where an item was defined. * For example, suppose a class is defined as "export default class MyClass { }" * but exported from the package's index.ts like this: * @@ -34,23 +35,35 @@ export class TypeScriptHelpers { return current; } - public static getImmediateAliasedSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol { - return (typeChecker as any).getImmediateAliasedSymbol(symbol); // tslint:disable-line:no-any - } - /** - * Returns the Symbol for the provided Declaration. This is a workaround for a missing - * feature of the TypeScript Compiler API. It is the only apparent way to reach - * certain data structures, and seems to always work, but is not officially documented. - * - * @returns The associated Symbol. If there is no semantic information (e.g. if the - * declaration is an extra semicolon somewhere), then "undefined" is returned. + * Returns true if the specified symbol is an ambient declaration. */ - public static tryGetSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol | undefined { - /* tslint:disable:no-any */ - const symbol: ts.Symbol = (declaration as any).symbol; - /* tslint:enable:no-any */ - return symbol; + public static isAmbient(symbol: ts.Symbol, typeChecker: ts.TypeChecker): boolean { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(symbol, typeChecker); + + if (followedSymbol.declarations && followedSymbol.declarations.length > 0) { + const firstDeclaration: ts.Declaration = followedSymbol.declarations[0]; + + // Test 1: Are we inside the sinister "declare global {" construct? + const highestModuleDeclaration: ts.ModuleDeclaration | undefined + = TypeScriptHelpers.findHighestParent(firstDeclaration, ts.SyntaxKind.ModuleDeclaration); + if (highestModuleDeclaration) { + if (highestModuleDeclaration.name.getText().trim() === 'global') { + return true; + } + } + + // Test 2: Otherwise, the main heuristic for ambient declarations is by looking at the + // ts.SyntaxKind.SourceFile node to see whether it has a symbol or not (i.e. whether it + // is acting as a module or not). + const sourceFile: ts.SourceFile = firstDeclaration.getSourceFile(); + + if (!!typeChecker.getSymbolAtLocation(sourceFile)) { + return false; + } + } + + return true; } /** @@ -58,7 +71,7 @@ export class TypeScriptHelpers { * cannot be found. */ public static getSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol { - const symbol: ts.Symbol | undefined = TypeScriptHelpers.tryGetSymbolForDeclaration(declaration); + const symbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(declaration); if (!symbol) { throw new Error(TypeScriptMessageFormatter.formatFileAndLineNumber(declaration) + ': ' + 'Unable to determine semantic information for this declaration'); @@ -66,15 +79,16 @@ export class TypeScriptHelpers { return symbol; } - /** - * Retrieves the comment ranges associated with the specified node. - */ - public static getJSDocCommentRanges(node: ts.Node, text: string): ts.CommentRange[] | undefined { - // Compiler internal: - // https://github.com/Microsoft/TypeScript/blob/v2.4.2/src/compiler/utilities.ts#L616 + // Return name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point' + public static getModuleSpecifier(declarationWithModuleSpecifier: ts.ImportDeclaration + | ts.ExportDeclaration): string | undefined { - // tslint:disable-next-line:no-any - return (ts as any).getJSDocCommentRanges.apply(this, arguments); + if (declarationWithModuleSpecifier.moduleSpecifier + && ts.isStringLiteralLike(declarationWithModuleSpecifier.moduleSpecifier)) { + return TypeScriptInternals.getTextOfIdentifierOrLiteral(declarationWithModuleSpecifier.moduleSpecifier); + } + + return undefined; } /** diff --git a/apps/api-extractor/src/analyzer/TypeScriptInternals.ts b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts new file mode 100644 index 00000000000..55b7018fb5d --- /dev/null +++ b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// tslint:disable:no-any + +import * as ts from 'typescript'; + +export class TypeScriptInternals { + + public static getImmediateAliasedSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol { + // Compiler internal: + // https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/checker.ts + return (typeChecker as any).getImmediateAliasedSymbol(symbol); // tslint:disable-line:no-any + } + + /** + * Returns the Symbol for the provided Declaration. This is a workaround for a missing + * feature of the TypeScript Compiler API. It is the only apparent way to reach + * certain data structures, and seems to always work, but is not officially documented. + * + * @returns The associated Symbol. If there is no semantic information (e.g. if the + * declaration is an extra semicolon somewhere), then "undefined" is returned. + */ + public static tryGetSymbolForDeclaration(declaration: ts.Declaration): ts.Symbol | undefined { + const symbol: ts.Symbol = (declaration as any).symbol; + return symbol; + } + + /** + * Retrieves the comment ranges associated with the specified node. + */ + public static getJSDocCommentRanges(node: ts.Node, text: string): ts.CommentRange[] | undefined { + // Compiler internal: + // https://github.com/Microsoft/TypeScript/blob/v2.4.2/src/compiler/utilities.ts#L616 + + return (ts as any).getJSDocCommentRanges.apply(this, arguments); + } + + /** + * Retrieves the (unescaped) value of an string literal, numeric literal, or identifier. + */ + public static getTextOfIdentifierOrLiteral(node: ts.Identifier | ts.StringLiteralLike | ts.NumericLiteral): string { + // Compiler internal: + // https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/utilities.ts#L2721 + + return (ts as any).getTextOfIdentifierOrLiteral(node); + } + + /** + * Retrieves the (cached) module resolution information for a module name that was exported from a SourceFile. + * The compiler populates this cache as part of analyzing the source file. + */ + public static getResolvedModule(sourceFile: ts.SourceFile, moduleNameText: string): ts.ResolvedModuleFull + | undefined { + + // Compiler internal: + // https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/utilities.ts#L218 + + return (ts as any).getResolvedModule(sourceFile, moduleNameText); + } +} diff --git a/apps/api-extractor/src/api/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts index 2ced10321f1..20341f32aa9 100644 --- a/apps/api-extractor/src/api/Extractor.ts +++ b/apps/api-extractor/src/api/Extractor.ts @@ -437,7 +437,8 @@ export class Extractor { // Write the actual file FileSystem.writeFile(actualApiReviewPath, actualApiReviewContent, { - ensureFolderExists: true + ensureFolderExists: true, + convertLineEndings: NewlineKind.CrLf }); // Compare it against the expected file @@ -458,7 +459,10 @@ export class Extractor { this._monitoredLogger.logWarning('You have changed the public API signature for this project.' + ` Updating ${expectedApiReviewShortPath}`); - FileSystem.writeFile(expectedApiReviewPath, actualApiReviewContent); + FileSystem.writeFile(expectedApiReviewPath, actualApiReviewContent, { + ensureFolderExists: true, + convertLineEndings: NewlineKind.CrLf + }); } } else { this._monitoredLogger.logVerbose(`The API signature is up to date: ${actualApiReviewShortPath}`); diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 0c4e103d8c2..7290f5c9bc7 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -20,7 +20,7 @@ import { import { TypeScriptMessageFormatter } from '../analyzer/TypeScriptMessageFormatter'; import { CollectorEntity } from './CollectorEntity'; import { AstSymbolTable } from '../analyzer/AstSymbolTable'; -import { AstEntryPoint } from '../analyzer/AstEntryPoint'; +import { AstModule } from '../analyzer/AstModule'; import { AstSymbol } from '../analyzer/AstSymbol'; import { ReleaseTag } from '../aedoc/ReleaseTag'; import { AstDeclaration } from '../analyzer/AstDeclaration'; @@ -29,6 +29,7 @@ import { CollectorPackage } from './CollectorPackage'; import { PackageDocComment } from '../aedoc/PackageDocComment'; import { DeclarationMetadata } from './DeclarationMetadata'; import { SymbolMetadata } from './SymbolMetadata'; +import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; /** * Options for Collector constructor. @@ -81,12 +82,14 @@ export class Collector { private readonly _tsdocParser: tsdoc.TSDocParser; - private _astEntryPoint: AstEntryPoint | undefined; + private _astEntryPoint: AstModule | undefined; private readonly _entities: CollectorEntity[] = []; private readonly _entitiesByAstSymbol: Map = new Map(); private readonly _entitiesBySymbol: Map = new Map(); + private readonly _starExportedExternalModulePaths: string[] = []; + private readonly _dtsTypeReferenceDirectives: Set = new Set(); private readonly _dtsLibReferenceDirectives: Set = new Set(); @@ -150,6 +153,14 @@ export class Collector { return this._entities; } + /** + * A list of module specifiers (e.g. `"@microsoft/node-core-library/lib/FileSystem"`) that should be emitted + * as star exports (e.g. `export * from "@microsoft/node-core-library/lib/FileSystem"`). + */ + public get starExportedExternalModulePaths(): ReadonlyArray { + return this._starExportedExternalModulePaths; + } + /** * Perform the analysis. */ @@ -167,7 +178,9 @@ export class Collector { } // Build the entry point - const astEntryPoint: AstEntryPoint = this.astSymbolTable.fetchEntryPoint(this.package.entryPointSourceFile); + const astEntryPoint: AstModule = this.astSymbolTable.fetchEntryPointModule( + this.package.entryPointSourceFile); + this._astEntryPoint = astEntryPoint; const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( this.package.entryPointSourceFile, this); @@ -183,10 +196,8 @@ export class Collector { const exportedAstSymbols: AstSymbol[] = []; // Create a CollectorEntity for each top-level export - for (const exportedMember of astEntryPoint.exportedMembers) { - const astSymbol: AstSymbol = exportedMember.astSymbol; - - this._createEntityForSymbol(exportedMember.astSymbol, exportedMember.name); + for (const [exportName, astSymbol] of astEntryPoint.exportedSymbols) { + this._createEntityForSymbol(astSymbol, exportName); exportedAstSymbols.push(astSymbol); } @@ -203,9 +214,16 @@ export class Collector { this._makeUniqueNames(); + for (const starExportedExternalModule of astEntryPoint.starExportedExternalModules) { + if (starExportedExternalModule.externalModulePath !== undefined) { + this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath); + } + } + Sort.sortBy(this._entities, x => x.getSortKey()); Sort.sortSet(this._dtsTypeReferenceDirectives); Sort.sortSet(this._dtsLibReferenceDirectives); + this._starExportedExternalModulePaths.sort(); } public tryGetEntityBySymbol(symbol: ts.Symbol): CollectorEntity | undefined { @@ -248,27 +266,17 @@ export class Collector { let entity: CollectorEntity | undefined = this._entitiesByAstSymbol.get(astSymbol); if (!entity) { - entity = new CollectorEntity({ - astSymbol: astSymbol, - originalName: exportedName || astSymbol.localName, - exported: !!exportedName - }); + entity = new CollectorEntity(astSymbol); this._entitiesByAstSymbol.set(astSymbol, entity); this._entitiesBySymbol.set(astSymbol.followedSymbol, entity); this._entities.push(entity); this._collectReferenceDirectives(astSymbol); - } else { - if (exportedName) { - if (!entity.exported) { - throw new InternalError('CollectorEntity should have been marked as exported'); - } - if (entity.originalName !== exportedName) { - throw new InternalError(`The symbol ${exportedName} was also exported as ${entity.originalName};` - + ` this is not supported yet`); - } - } + } + + if (exportedName) { + entity.addExportName(exportedName); } } @@ -294,31 +302,39 @@ export class Collector { // First collect the explicit package exports (named) for (const entity of this._entities) { - if (entity.exported && entity.originalName !== ts.InternalSymbolName.Default) { - - if (usedNames.has(entity.originalName)) { + for (const exportName of entity.exportNames) { + if (usedNames.has(exportName)) { // This should be impossible - throw new InternalError(`A package cannot have two exports with the name ${entity.originalName}`); + throw new InternalError(`A package cannot have two exports with the name "${exportName}"`); } - entity.nameForEmit = entity.originalName; - - usedNames.add(entity.nameForEmit); + usedNames.add(exportName); } } // Next generate unique names for the non-exports that will be emitted (and the default export) for (const entity of this._entities) { - if (!entity.exported || entity.originalName === ts.InternalSymbolName.Default) { - let suffix: number = 1; - entity.nameForEmit = entity.astSymbol.localName; - while (usedNames.has(entity.nameForEmit)) { - entity.nameForEmit = `${entity.astSymbol.localName}_${++suffix}`; - } + // If this entity is exported exactly once, then emit the exported name + if (entity.singleExportName !== undefined && entity.singleExportName !== ts.InternalSymbolName.Default) { + entity.nameForEmit = entity.singleExportName; + continue; + } + + // If the localName happens to be the same as one of the exports, then emit that name + if (entity.exportNames.has(entity.astSymbol.localName)) { + entity.nameForEmit = entity.astSymbol.localName; + continue; + } - usedNames.add(entity.nameForEmit); + // In all other cases, generate a unique name based on the localName + let suffix: number = 1; + let nameForEmit: string = entity.astSymbol.localName; + while (usedNames.has(nameForEmit)) { + nameForEmit = `${entity.astSymbol.localName}_${++suffix}`; } + entity.nameForEmit = nameForEmit; + usedNames.add(nameForEmit); } } @@ -507,7 +523,7 @@ export class Collector { } const sourceFileText: string = declaration.getSourceFile().text; - const ranges: ts.CommentRange[] = TypeScriptHelpers.getJSDocCommentRanges(nodeForComment, sourceFileText) || []; + const ranges: ts.CommentRange[] = TypeScriptInternals.getJSDocCommentRanges(nodeForComment, sourceFileText) || []; if (ranges.length === 0) { return undefined; diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index d194831e6e3..d49cafb9910 100644 --- a/apps/api-extractor/src/collector/CollectorEntity.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -1,21 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import * as ts from 'typescript'; + import { AstSymbol } from '../analyzer/AstSymbol'; import { Collector } from './Collector'; +import { Sort } from '@microsoft/node-core-library'; /** - * Constructor options for CollectorEntity - */ -export interface ICollectorEntityOptions { - readonly astSymbol: AstSymbol; - readonly originalName: string; - readonly exported: boolean; -} - -/** - * This is a data structure used by DtsRollupGenerator to track an AstSymbol that may be - * emitted in the *.d.ts file. + * This is a data structure used by the Collector to track an AstSymbol that may be emitted in the *.d.ts file. + * * @remarks * The additional contextual state beyond AstSymbol is: * - Whether it's an export of this entry point or not @@ -27,28 +21,22 @@ export class CollectorEntity { */ public readonly astSymbol: AstSymbol; - /** - * The original name, prior to any renaming by DtsRollupGenerator._makeUniqueNames() - */ - public readonly originalName: string; - - /** - * Whether this API item is exported by the *.t.s file - */ - public readonly exported: boolean; + private _exportNames: Set = new Set(); + private _exportNamesSorted: boolean = false; + private _singleExportName: string | undefined = undefined; private _nameForEmit: string | undefined = undefined; private _sortKey: string | undefined = undefined; - public constructor(options: ICollectorEntityOptions) { - this.astSymbol = options.astSymbol; - this.originalName = options.originalName; - this.exported = options.exported; + public constructor(astSymbol: AstSymbol) { + this.astSymbol = astSymbol; } /** - * The originalName, possibly renamed to ensure that all the top-level exports have unique names. + * The declaration name that will be emitted in a .d.ts rollup. For non-exported declarations, + * Collector._makeUniqueNames() may need to rename the declaration to avoid conflicts with other declarations + * in that module. */ public get nameForEmit(): string | undefined { return this._nameForEmit; @@ -59,13 +47,72 @@ export class CollectorEntity { this._sortKey = undefined; // invalidate the cached value } + /** + * If this symbol is exported from the entry point, the list of export names. + * + * @remarks + * Note that a given symbol may be exported more than once: + * ``` + * class X { } + * export { X } + * export { X as Y } + * ``` + */ + public get exportNames(): ReadonlySet { + if (!this._exportNamesSorted) { + Sort.sortSet(this._exportNames); + this._exportNamesSorted = true; + } + return this._exportNames; + } + + /** + * If exportNames contains only one string, then singleExportName is that string. + * In all other cases, it is undefined. + */ + public get singleExportName(): string | undefined { + return this._singleExportName; + } + + /** + * This is true if exportNames contains only one string, and the declaration can be exported using the inline syntax + * such as "export class X { }" instead of "export { X }". + */ + public get shouldInlineExport(): boolean { + return this._singleExportName !== undefined + && this._singleExportName !== ts.InternalSymbolName.Default + && this.astSymbol.astImport === undefined; + } + + /** + * Returns true if this symbol is an export for the entry point being analyzed. + */ + public get exported(): boolean { + return this.exportNames.size > 0; + } + + /** + * Adds a new exportName to the exportNames set. + */ + public addExportName(exportName: string): void { + if (!this._exportNames.has(exportName)) { + this._exportNamesSorted = false; + this._exportNames.add(exportName); + + if (this._exportNames.size === 1) { + this._singleExportName = exportName; + } else { + this._singleExportName = undefined; + } + } + } + /** * A sorting key used by DtsRollupGenerator._makeUniqueNames() */ public getSortKey(): string { if (!this._sortKey) { - const name: string = this.nameForEmit || this.originalName; - this._sortKey = Collector.getSortKeyIgnoringUnderscore(name); + this._sortKey = Collector.getSortKeyIgnoringUnderscore(this.nameForEmit || this.astSymbol.localName); } return this._sortKey; } diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts index af93271619b..f61bc211b04 100644 --- a/apps/api-extractor/src/generators/ApiModelGenerator.ts +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -64,7 +64,13 @@ export class ApiModelGenerator { for (const entity of this._collector.entities) { for (const astDeclaration of entity.astSymbol.astDeclarations) { if (entity.exported) { - this._processDeclaration(astDeclaration, entity.nameForEmit, apiEntryPoint); + if (!entity.astSymbol.imported) { + this._processDeclaration(astDeclaration, entity.nameForEmit, apiEntryPoint); + } else { + // TODO: Figure out how to represent reexported definitions. Basically we need to introduce a new + // ApiItem subclass for "export alias", similar to a type alias, but representing declarations of the + // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. + } } } } diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index 77feaf97f79..451fa38f030 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -14,7 +14,6 @@ import { ReleaseTag } from '../aedoc/ReleaseTag'; import { AstImport } from '../analyzer/AstImport'; import { CollectorEntity } from '../collector/CollectorEntity'; import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { SymbolAnalyzer } from '../analyzer/SymbolAnalyzer'; import { DeclarationMetadata } from '../collector/DeclarationMetadata'; /** @@ -95,13 +94,6 @@ export class DtsRollupGenerator { } indentedWriter.writeLine(` from '${astImport.modulePath}';`); - if (entity.exported) { - // We write re-export as two lines: `import { Mod } from 'package'; export { Mod };`, - // instead of a single line `export { Mod } from 'package';`. - // Because this variable may be used by others, and we cannot know it. - // so we always keep the `import ...` declaration, for now. - indentedWriter.writeLine(`export { ${entity.nameForEmit} };`); - } } } } @@ -109,26 +101,44 @@ export class DtsRollupGenerator { // Emit the regular declarations for (const entity of collector.entities) { if (!entity.astSymbol.astImport) { - const releaseTag: ReleaseTag = collector.fetchMetadata(entity.astSymbol).releaseTag; - if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + indentedWriter.writeLine(); + indentedWriter.writeLine(`/* Excluded from this release type: ${entity.nameForEmit} */`); + continue; + } - // Emit all the declarations for this entry - for (const astDeclaration of entity.astSymbol.astDeclarations || []) { + // Emit all the declarations for this entry + for (const astDeclaration of entity.astSymbol.astDeclarations || []) { - indentedWriter.writeLine(); + indentedWriter.writeLine(); - const span: Span = new Span(astDeclaration.declaration); - DtsRollupGenerator._modifySpan(collector, span, entity, astDeclaration, dtsKind); - indentedWriter.writeLine(span.getModifiedText()); + const span: Span = new Span(astDeclaration.declaration); + DtsRollupGenerator._modifySpan(collector, span, entity, astDeclaration, dtsKind); + indentedWriter.writeLine(span.getModifiedText()); + } + } + + if (!entity.shouldInlineExport) { + for (const exportName of entity.exportNames) { + if (exportName === ts.InternalSymbolName.Default) { + indentedWriter.writeLine(`export default ${entity.nameForEmit};`); + } else if (entity.nameForEmit !== exportName) { + indentedWriter.writeLine(`export { ${entity.nameForEmit} as ${exportName} }`); + } else { + indentedWriter.writeLine(`export { ${exportName} }`); } - } else { - indentedWriter.writeLine(); - indentedWriter.writeLine(`/* Excluded from this release type: ${entity.nameForEmit} */`); } } } + if (collector.starExportedExternalModulePaths.length > 0) { + indentedWriter.writeLine(); + for (const starExportedExternalModulePath of collector.starExportedExternalModulePaths) { + indentedWriter.writeLine(`export * from "${starExportedExternalModulePath}";`); + } + } + // Emit "export { }" which is a special directive that prevents consumers from importing declarations // that don't have an explicit "export" modifier. indentedWriter.writeLine(); @@ -178,12 +188,8 @@ export class DtsRollupGenerator { replacedModifiers += 'declare '; } - if (entity.exported) { - if (entity.originalName === ts.InternalSymbolName.Default) { - (span.parent || span).modification.suffix = `\nexport default ${entity.nameForEmit};`; - } else { - replacedModifiers = 'export ' + replacedModifiers; - } + if (entity.shouldInlineExport) { + replacedModifiers = 'export ' + replacedModifiers; } if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { @@ -219,12 +225,8 @@ export class DtsRollupGenerator { span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; span.modification.suffix = ';'; - if (entity.exported) { - if (entity.originalName === ts.InternalSymbolName.Default) { - span.modification.suffix += `\nexport default ${entity.nameForEmit};`; - } else { - span.modification.prefix = 'export ' + span.modification.prefix; - } + if (entity.shouldInlineExport) { + span.modification.prefix = 'export ' + span.modification.prefix; } const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration); @@ -277,7 +279,7 @@ export class DtsRollupGenerator { // Should we trim this node? let trimmed: boolean = false; - if (SymbolAnalyzer.isAstDeclaration(child.kind)) { + if (AstDeclaration.isSupportedSyntaxKind(child.kind)) { childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); const releaseTag: ReleaseTag = collector.fetchMetadata(childAstDeclaration.astSymbol).releaseTag; diff --git a/apps/api-extractor/src/generators/ReviewFileGenerator.ts b/apps/api-extractor/src/generators/ReviewFileGenerator.ts index f82915a3533..5a4b5cec788 100644 --- a/apps/api-extractor/src/generators/ReviewFileGenerator.ts +++ b/apps/api-extractor/src/generators/ReviewFileGenerator.ts @@ -9,11 +9,11 @@ import { Span } from '../analyzer/Span'; import { CollectorEntity } from '../collector/CollectorEntity'; import { AstDeclaration } from '../analyzer/AstDeclaration'; import { StringBuilder } from '@microsoft/tsdoc'; -import { SymbolAnalyzer } from '../analyzer/SymbolAnalyzer'; import { DeclarationMetadata } from '../collector/DeclarationMetadata'; import { SymbolMetadata } from '../collector/SymbolMetadata'; import { ReleaseTag } from '../aedoc/ReleaseTag'; import { Text, InternalError } from '@microsoft/node-core-library'; +import { AstImport } from '../analyzer/AstImport'; export class ReviewFileGenerator { /** @@ -35,19 +35,42 @@ export class ReviewFileGenerator { for (const entity of collector.entities) { if (entity.exported) { - // Emit all the declarations for this entry - for (const astDeclaration of entity.astSymbol.astDeclarations || []) { + if (!entity.astSymbol.astImport) { + // Emit all the declarations for this entry + for (const astDeclaration of entity.astSymbol.astDeclarations || []) { - output.append(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration)); + output.append(ReviewFileGenerator._getAedocSynopsis(collector, astDeclaration)); - const span: Span = new Span(astDeclaration.declaration); - ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration); - span.writeModifiedText(output); - output.append('\n\n'); + const span: Span = new Span(astDeclaration.declaration); + ReviewFileGenerator._modifySpan(collector, span, entity, astDeclaration); + span.writeModifiedText(output); + output.append('\n\n'); + } + } else { + // This definition is reexported from another package, so write it as an "export" line + // In general, we don't report on external packages; if that's important we assume API Extractor + // would be enabled for the upstream project. But see GitHub issue #896 for a possible exception. + const astImport: AstImport = entity.astSymbol.astImport; + + if (astImport.exportName === '*') { + output.append(`export * as ${entity.nameForEmit}`); + } else if (entity.nameForEmit !== astImport.exportName) { + output.append(`export { ${astImport.exportName} as ${entity.nameForEmit} }`); + } else { + output.append(`export { ${astImport.exportName} }`); + } + output.append(` from '${astImport.modulePath}';\n`); } } } + if (collector.starExportedExternalModulePaths.length > 0) { + output.append('\n'); + for (const starExportedExternalModulePath of collector.starExportedExternalModulePaths) { + output.append(`export * from "${starExportedExternalModulePath}";\n`); + } + } + if (collector.package.tsdocComment === undefined) { output.append('\n'); ReviewFileGenerator._writeLineAsComment(output, '(No @packageDocumentation comment for this package)'); @@ -85,7 +108,7 @@ export class ReviewFileGenerator { case ts.SyntaxKind.SyntaxList: if (span.parent) { - if (SymbolAnalyzer.isAstDeclaration(span.parent.kind)) { + if (AstDeclaration.isSupportedSyntaxKind(span.parent.kind)) { // If the immediate parent is an API declaration, and the immediate children are API declarations, // then sort the children alphabetically sortChildren = true; @@ -153,7 +176,7 @@ export class ReviewFileGenerator { for (const child of span.children) { let childAstDeclaration: AstDeclaration = astDeclaration; - if (SymbolAnalyzer.isAstDeclaration(child.kind)) { + if (AstDeclaration.isSupportedSyntaxKind(child.kind)) { childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); if (sortChildren) { diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.json index cab56916e16..9a06b0b7011 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.json @@ -16,13 +16,36 @@ "members": [ { "kind": "Class", - "canonicalReference": "(X:class)", + "canonicalReference": "(A:class)", "docComment": "/**\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", "text": "declare class " }, + { + "kind": "Reference", + "text": "A" + }, + { + "kind": "Content", + "text": " " + } + ], + "releaseTag": "Public", + "name": "A", + "members": [], + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "(X:class)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare class " + }, { "kind": "Reference", "text": "X" diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.ts index 8fd670992b9..bd32ea67507 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/api-extractor-scenarios.api.ts @@ -1,3 +1,7 @@ +// @public (undocumented) +declare class A { +} + // @public (undocumented) declare class X { } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/rollup.d.ts index 78633c812e7..537a56a934d 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportDuplicate1/rollup.d.ts @@ -1,6 +1,14 @@ /** @public */ -export declare class X { +declare class A { } +export { A as B } +export { A as C } + +/** @public */ +declare class X { +} +export { X } +export { X as Y } export { } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.json index cad382ad5bf..09888e8482d 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.json @@ -13,100 +13,7 @@ "kind": "EntryPoint", "canonicalReference": "", "name": "", - "members": [ - { - "kind": "Interface", - "canonicalReference": "(DoubleRenamedLib2Class:interface)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare interface " - }, - { - "kind": "Reference", - "text": "Lib2Interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "DoubleRenamedLib2Class", - "members": [], - "extendsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "(Lib1Class:class)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare class " - }, - { - "kind": "Reference", - "text": "Lib1Class" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib1Class", - "members": [], - "implementsTokenRanges": [] - }, - { - "kind": "Interface", - "canonicalReference": "(Lib1Interface:interface)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare interface " - }, - { - "kind": "Reference", - "text": "Lib1Interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib1Interface", - "members": [], - "extendsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "(RenamedLib2Class:class)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare class " - }, - { - "kind": "Reference", - "text": "Lib2Class" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "RenamedLib2Class", - "members": [], - "implementsTokenRanges": [] - } - ] + "members": [] } ] } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.ts index 001501d55a9..6218f37e0a7 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/api-extractor-scenarios.api.ts @@ -1,18 +1,6 @@ -// @public (undocumented) -declare interface DoubleRenamedLib2Class { -} - -// @public (undocumented) -declare class Lib1Class { -} - -// @public (undocumented) -declare interface Lib1Interface { -} - -// @public (undocumented) -declare class RenamedLib2Class { -} - +export { Lib2Interface as DoubleRenamedLib2Class } from 'api-extractor-lib2-test'; +export { Lib1Class } from 'api-extractor-lib1-test'; +export { Lib1Interface } from 'api-extractor-lib1-test'; +export { Lib2Class as RenamedLib2Class } from 'api-extractor-lib2-test'; // (No @packageDocumentation comment for this package) diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/rollup.d.ts index 69140abd56a..034539e5c2b 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportedExternal/rollup.d.ts @@ -1,10 +1,10 @@ import { Lib2Interface as DoubleRenamedLib2Class } from 'api-extractor-lib2-test'; -export { DoubleRenamedLib2Class }; import { Lib1Class } from 'api-extractor-lib1-test'; -export { Lib1Class }; import { Lib1Interface } from 'api-extractor-lib1-test'; -export { Lib1Interface }; import { Lib2Class as RenamedLib2Class } from 'api-extractor-lib2-test'; -export { RenamedLib2Class }; +export { DoubleRenamedLib2Class } +export { Lib1Class } +export { Lib1Interface } +export { RenamedLib2Class } export { } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.json index e9c3832f46b..fe805c03a70 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.json @@ -36,98 +36,6 @@ "name": "A", "members": [], "implementsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "(Lib1Class:class)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare class " - }, - { - "kind": "Reference", - "text": "Lib1Class" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib1Class", - "members": [], - "implementsTokenRanges": [] - }, - { - "kind": "Interface", - "canonicalReference": "(Lib1Interface:interface)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare interface " - }, - { - "kind": "Reference", - "text": "Lib1Interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib1Interface", - "members": [], - "extendsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "(Lib2Class:class)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare class " - }, - { - "kind": "Reference", - "text": "Lib2Class" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib2Class", - "members": [], - "implementsTokenRanges": [] - }, - { - "kind": "Interface", - "canonicalReference": "(Lib2Interface:interface)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare interface " - }, - { - "kind": "Reference", - "text": "Lib2Interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib2Interface", - "members": [], - "extendsTokenRanges": [] } ] } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.ts index 58c7024adcc..d3cc3caaf32 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/api-extractor-scenarios.api.ts @@ -2,21 +2,8 @@ declare class A { } -// @public (undocumented) -declare class Lib1Class { -} - -// @public (undocumented) -declare interface Lib1Interface { -} - -// @public (undocumented) -declare class Lib2Class { -} - -// @public (undocumented) -declare interface Lib2Interface { -} +export * from "api-extractor-lib1-test"; +export * from "api-extractor-lib2-test"; // (No @packageDocumentation comment for this package) diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/rollup.d.ts index 68e902a19f9..8e6bcc58b9b 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/rollup.d.ts @@ -3,20 +3,7 @@ export declare class A { } -/** @public */ -export declare class Lib1Class { -} - -/** @public */ -export declare interface Lib1Interface { -} - -/** @public */ -export declare class Lib2Class { -} - -/** @public */ -export declare interface Lib2Interface { -} +export * from "api-extractor-lib1-test"; +export * from "api-extractor-lib2-test"; export { } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.json index 482892907d1..fe805c03a70 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.json @@ -36,29 +36,6 @@ "name": "A", "members": [], "implementsTokenRanges": [] - }, - { - "kind": "Class", - "canonicalReference": "(Lib1Class:class)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare class " - }, - { - "kind": "Reference", - "text": "Lib1Class" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "Lib1Class", - "members": [], - "implementsTokenRanges": [] } ] } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.ts index 74b9d6fec05..87431d028c5 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/api-extractor-scenarios.api.ts @@ -2,9 +2,7 @@ declare class A { } -// @public (undocumented) -declare class Lib1Class { -} - +export { Lib1Class } from 'api-extractor-lib1-test'; +export { Lib2Interface as RenamedLib2Interface } from 'api-extractor-lib2-test'; // (No @packageDocumentation comment for this package) diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/rollup.d.ts index 4f2d0815801..f4a993e0329 100644 --- a/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar3/rollup.d.ts @@ -1,10 +1,10 @@ +import { Lib1Class } from 'api-extractor-lib1-test'; +import { Lib2Interface as RenamedLib2Interface } from 'api-extractor-lib2-test'; /** @public */ export declare class A { } - -/** @public */ -export declare class Lib1Class { -} +export { Lib1Class } +export { RenamedLib2Interface } export { } diff --git a/build-tests/api-extractor-scenarios/src/exportDuplicate1/index.ts b/build-tests/api-extractor-scenarios/src/exportDuplicate1/index.ts index 151d4a08cae..7fb7589c0c4 100644 --- a/build-tests/api-extractor-scenarios/src/exportDuplicate1/index.ts +++ b/build-tests/api-extractor-scenarios/src/exportDuplicate1/index.ts @@ -2,10 +2,10 @@ // See LICENSE in the project root for license information. /** @public */ -class X { -} +export class X { } +export { X as Y } -export { X } - -// TODO: "Internal Error: The symbol Y was also exported as X; this is not supported yet" -// export { X as Y} +/** @public */ +class A { } +export { A as B } +export { A as C } diff --git a/build-tests/api-extractor-scenarios/src/exportStar3/index.ts b/build-tests/api-extractor-scenarios/src/exportStar3/index.ts index 7bab9d0eb46..c99fb5bab9c 100644 --- a/build-tests/api-extractor-scenarios/src/exportStar3/index.ts +++ b/build-tests/api-extractor-scenarios/src/exportStar3/index.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -export { A, Lib1Class } from './reexportStar'; +export { A, Lib1Class, Lib2Interface as RenamedLib2Interface } from './reexportStar'; diff --git a/build-tests/api-extractor-scenarios/src/exportStar3/reexportStar.ts b/build-tests/api-extractor-scenarios/src/exportStar3/reexportStar.ts index f2ed3c7966f..b08a7ea8776 100644 --- a/build-tests/api-extractor-scenarios/src/exportStar3/reexportStar.ts +++ b/build-tests/api-extractor-scenarios/src/exportStar3/reexportStar.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. export * from 'api-extractor-lib1-test'; +export * from 'api-extractor-lib2-test'; /** @public */ export class A { } diff --git a/build-tests/api-extractor-scenarios/src/runScenarios.ts b/build-tests/api-extractor-scenarios/src/runScenarios.ts index 1ad27389ee7..770e6773fc8 100644 --- a/build-tests/api-extractor-scenarios/src/runScenarios.ts +++ b/build-tests/api-extractor-scenarios/src/runScenarios.ts @@ -76,8 +76,13 @@ export function runScenarios(buildConfigPath: string): void { // See GitHub issue https://github.com/Microsoft/web-build-tools/issues/1018 FileSystem.writeFile(`./etc/test-outputs/${scenarioFolderName}/api-extractor-scenarios.api.ts`, '', { ensureFolderExists: true }); + } + + for (const scenarioFolderName of buildConfig.scenarioFolderNames) { + const apiExtractorJsonPath: string = `./temp/configs/api-extractor-${scenarioFolderName}.json`; // Run the API Extractor command-line executeCommand(apiExtractorBinary, ['run', '--local', '--config', apiExtractorJsonPath]); } + } diff --git a/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts b/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts index 014c0cb880e..dc4544c9291 100644 --- a/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts +++ b/build-tests/api-extractor-test-02/dist/beta/api-extractor-test-02.d.ts @@ -9,7 +9,6 @@ import { ISimpleInterface } from 'api-extractor-test-01'; import { ReexportedClass as RenamedReexportedClass3 } from 'api-extractor-test-01'; -export { RenamedReexportedClass3 }; import * as semver1 from 'semver'; /** @@ -44,6 +43,7 @@ export declare function importedModuleAsGenericParameter(): GenericInterface