From bced506c8e72916e548e27ce4cf9079fb9fd25bd Mon Sep 17 00:00:00 2001 From: pgonzal Date: Thu, 20 Dec 2018 01:41:27 -0800 Subject: [PATCH 01/26] Initial prototype of improving the symbol analyzer to support "export * from" constructs --- .../src/analyzer/AstSymbolTable.ts | 111 +++++++++++++++++- .../src/analyzer/SymbolAnalyzer.ts | 38 +----- .../src/analyzer/TypeScriptHelpers.ts | 37 ++++++ 3 files changed, 150 insertions(+), 36 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 238400c9cdc..d2839b968c9 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -76,10 +76,14 @@ export class AstSymbolTable { throw new Error('Unable to find a root declaration for ' + sourceFile.fileName); } - const exportSymbols: ts.Symbol[] = this._typeChecker.getExportsOfModule(rootFileSymbol) || []; - const exportedMembers: IExportedMember[] = []; + const visitedModules: Set = new Set(); + this._collectExportsFromModule(exportedMembers, rootFileSymbol, visitedModules, undefined); + + /* + const exportSymbols: ts.Symbol[] = this._typeChecker.getExportsOfModule(rootFileSymbol) || []; +- for (const exportSymbol of exportSymbols) { const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(exportSymbol, true); @@ -91,6 +95,7 @@ export class AstSymbolTable { exportedMembers.push({ name: exportSymbol.name, astSymbol: astSymbol }); } + */ astEntryPoint = new AstEntryPoint({ exportedMembers }); this._astEntryPointsBySourceFile.set(sourceFile, astEntryPoint); @@ -98,6 +103,93 @@ export class AstSymbolTable { return astEntryPoint; } + private _collectExportsFromModule(exportedMembers: IExportedMember[], moduleSymbol: ts.Symbol, + visitedModules: Set, followedModulePath: string | undefined): void { + + // 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'". + if (visitedModules.has(moduleSymbol)) { + return; + } + visitedModules.add(moduleSymbol); + + if (moduleSymbol.exports) { + + for (const exportedSymbol of moduleSymbol.exports.values() as IterableIterator) { + if (exportedSymbol.escapedName !== ts.InternalSymbolName.ExportStar) { + const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(exportedSymbol, true, followedModulePath); + + if (!astSymbol) { + throw new Error('Unsupported export: ' + exportedSymbol.name); + } + + this.analyze(astSymbol); + + exportedMembers.push({ name: exportedSymbol.name, astSymbol: astSymbol }); + } else { + + // 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() || []) { + if (ts.isExportDeclaration(exportStarDeclaration)) { + this._collectExportsFromExportStar(exportedMembers, exportStarDeclaration, + visitedModules, followedModulePath); + } else { + // Ignore ExportDeclaration nodes that don't match the expected pattern + // TODO: Should we report a warning? + } + } + + } + } + + } + } + + private _collectExportsFromExportStar(exportedMembers: IExportedMember[], exportStarDeclaration: ts.ExportDeclaration, + visitedModules: Set, followedModulePath: string | undefined): void { + + // 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) { + // TODO: Should we report a warning? + return; + } + + // Are we leaving the main project for the first time? + if (followedModulePath === undefined) { + // Match: "@microsoft/sp-lodash-subset" or "lodash/has" + // but ignore: "../folder/LocalFile" + if (!ts.isExternalModuleNameRelative(moduleSpecifier)) { + // Yes, we are traversing into an external package. That becomes the followedModulePath for everything + // we encounter as we continue recursing + followedModulePath = moduleSpecifier; + } + } + + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.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 moduleSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(moduleSourceFile); + this._collectExportsFromModule(exportedMembers, moduleSymbol, visitedModules, followedModulePath); + } + /** * Ensures that AstSymbol.analyzed is true for the provided symbol. The operation * starts from the root symbol and then fills out all children of all declarations, and @@ -249,9 +341,20 @@ export class AstSymbolTable { return this._fetchAstSymbol(symbol, true); } - private _fetchAstSymbol(symbol: ts.Symbol, addIfMissing: boolean): AstSymbol | undefined { + private _fetchAstSymbol(symbol: ts.Symbol, addIfMissing: boolean, + followedModulePath?: string): AstSymbol | undefined { + const followAliasesResult: IFollowAliasesResult = SymbolAnalyzer.followAliases(symbol, this._typeChecker); + if (followedModulePath !== undefined) { + // FIX THIS + const exportName: string = followAliasesResult.astImport ? followAliasesResult.astImport.exportName + : followAliasesResult.localName; + + (followAliasesResult as any).astImport = new AstImport({ exportName, + modulePath: followedModulePath}); + } + const followedSymbol: ts.Symbol = followAliasesResult.followedSymbol; // Filter out symbols representing constructs that we don't care about @@ -305,7 +408,7 @@ export class AstSymbolTable { // 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; } diff --git a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts index 6bacaccc401..8eca923076f 100644 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts @@ -192,8 +192,7 @@ export class SymbolAnalyzer { // Examples: // " '@microsoft/sp-lodash-subset'" // " "lodash/has"" - const modulePath: string | undefined = SymbolAnalyzer._getPackagePathFromModuleSpecifier( - exportDeclaration.moduleSpecifier); + const modulePath: string | undefined = TypeScriptHelpers.getModuleSpecifier(exportDeclaration); if (modulePath) { return { @@ -284,17 +283,15 @@ export class SymbolAnalyzer { } if (importDeclaration.moduleSpecifier) { - // Examples: - // " '@microsoft/sp-lodash-subset'" - // " "lodash/has"" - const modulePath: string | undefined = SymbolAnalyzer._getPackagePathFromModuleSpecifier( - importDeclaration.moduleSpecifier); + const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(importDeclaration); - if (modulePath) { + // Match: "@microsoft/sp-lodash-subset" or "lodash/has" + // but ignore: "../folder/LocalFile" + if (moduleSpecifier && !ts.isExternalModuleNameRelative(moduleSpecifier)) { return { followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), localName: symbol.name, - astImport: new AstImport({ modulePath, exportName }), + astImport: new AstImport({ modulePath: moduleSpecifier, exportName }), isAmbient: false }; } @@ -304,27 +301,4 @@ export class SymbolAnalyzer { 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..3a360280a0d 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -66,6 +66,18 @@ export class TypeScriptHelpers { return symbol; } + // 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 { + + if (declarationWithModuleSpecifier.moduleSpecifier + && ts.isStringLiteralLike(declarationWithModuleSpecifier.moduleSpecifier)) { + return TypeScriptHelpers.getTextOfIdentifierOrLiteral(declarationWithModuleSpecifier.moduleSpecifier); + } + + return undefined; + } + /** * Retrieves the comment ranges associated with the specified node. */ @@ -77,6 +89,31 @@ export class TypeScriptHelpers { 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 + + // tslint:disable-next-line:no-any + 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 + + // tslint:disable-next-line:no-any + return (ts as any).getResolvedModule(sourceFile, moduleNameText); + } + /** * Returns an ancestor of "node", such that the ancestor, any intermediary nodes, * and the starting node match a list of expected kinds. Undefined is returned From adddd30dd7261b550ababcbb10f550f1c90050e8 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Thu, 20 Dec 2018 02:02:54 -0800 Subject: [PATCH 02/26] Ensure isExternalModuleNameRelative() is called consistently --- .../src/analyzer/SymbolAnalyzer.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts index 8eca923076f..f1c24ae0a77 100644 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts @@ -189,16 +189,13 @@ export class SymbolAnalyzer { } if (exportDeclaration.moduleSpecifier) { - // Examples: - // " '@microsoft/sp-lodash-subset'" - // " "lodash/has"" - const modulePath: string | undefined = TypeScriptHelpers.getModuleSpecifier(exportDeclaration); + const externalModulePath: string | undefined = SymbolAnalyzer._tryGetExternalModulePath(exportDeclaration); - if (modulePath) { + if (externalModulePath) { return { followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), localName: exportName, - astImport: new AstImport({ modulePath, exportName }), + astImport: new AstImport({ modulePath: externalModulePath, exportName }), isAmbient: false }; } @@ -283,15 +280,12 @@ export class SymbolAnalyzer { } if (importDeclaration.moduleSpecifier) { - const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(importDeclaration); - - // Match: "@microsoft/sp-lodash-subset" or "lodash/has" - // but ignore: "../folder/LocalFile" - if (moduleSpecifier && !ts.isExternalModuleNameRelative(moduleSpecifier)) { + const externalModulePath: string | undefined = SymbolAnalyzer._tryGetExternalModulePath(importDeclaration); + if (externalModulePath) { return { followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), localName: symbol.name, - astImport: new AstImport({ modulePath: moduleSpecifier, exportName }), + astImport: new AstImport({ modulePath: externalModulePath, exportName }), isAmbient: false }; } @@ -301,4 +295,18 @@ export class SymbolAnalyzer { return undefined; } + + private static _tryGetExternalModulePath(declarationWithModuleSpecifier: ts.ImportDeclaration + | ts.ExportDeclaration): string | undefined { + + const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(declarationWithModuleSpecifier); + + // Match: "@microsoft/sp-lodash-subset" or "lodash/has" + // but ignore: "../folder/LocalFile" + if (moduleSpecifier && !ts.isExternalModuleNameRelative(moduleSpecifier)) { + return moduleSpecifier; + } + + return undefined; + } } From 75d2a90e54ff86e1b6217775b76c9e5a72ed7e8f Mon Sep 17 00:00:00 2001 From: pgonzal Date: Sun, 23 Dec 2018 13:03:53 -0800 Subject: [PATCH 03/26] Fix up ApiModelGenerator and ReviewFileGenerator to handle reexported declarations (i.e. where both CollectorEntity.exported=true and AstSymbol.imported=true) --- .../src/generators/ApiModelGenerator.ts | 8 ++++- .../src/generators/ReviewFileGenerator.ts | 31 ++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) 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/ReviewFileGenerator.ts b/apps/api-extractor/src/generators/ReviewFileGenerator.ts index f82915a3533..43bd0f6f777 100644 --- a/apps/api-extractor/src/generators/ReviewFileGenerator.ts +++ b/apps/api-extractor/src/generators/ReviewFileGenerator.ts @@ -14,6 +14,7 @@ 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,15 +36,31 @@ 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`); } } } From d0f872e288750cb43f1f43be0b71ef4f0ef0cb46 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Sun, 13 Jan 2019 12:43:55 -0800 Subject: [PATCH 04/26] Update tests after rebase --- .../api-extractor-scenarios.api.json | 95 +------------------ .../api-extractor-scenarios.api.ts | 20 +--- .../api-extractor-scenarios.api.json | 92 ------------------ .../api-extractor-scenarios.api.ts | 20 +--- .../etc/test-outputs/exportStar2/rollup.d.ts | 24 ++--- 5 files changed, 17 insertions(+), 234 deletions(-) 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/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..ad598f92b8f 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,9 @@ declare class A { } -// @public (undocumented) -declare class Lib1Class { -} - -// @public (undocumented) -declare interface Lib1Interface { -} - -// @public (undocumented) -declare class Lib2Class { -} - -// @public (undocumented) -declare interface Lib2Interface { -} - +export { Lib1Class } from 'api-extractor-lib1-test'; +export { Lib1Interface } from 'api-extractor-lib1-test'; +export { Lib2Class } from 'api-extractor-lib2-test'; +export { Lib2Interface } 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..0433940f57e 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 @@ -1,22 +1,14 @@ +import { Lib1Class } from 'api-extractor-lib1-test'; +export { Lib1Class }; +import { Lib1Interface } from 'api-extractor-lib1-test'; +export { Lib1Interface }; +import { Lib2Class } from 'api-extractor-lib2-test'; +export { Lib2Class }; +import { Lib2Interface } from 'api-extractor-lib2-test'; +export { Lib2Interface }; /** @public */ 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 432d9772d556d97e94dc689e10357d2b5d4eda0f Mon Sep 17 00:00:00 2001 From: pgonzal Date: Sun, 13 Jan 2019 16:15:46 -0800 Subject: [PATCH 05/26] Generalize CollectorEntity to support a given declaration being exported multiple times (fixes GitHub issue #950) --- apps/api-extractor/.vscode/launch.json | 34 +++--- apps/api-extractor/src/collector/Collector.ts | 56 +++++----- .../src/collector/CollectorEntity.ts | 101 +++++++++++++----- .../src/generators/DtsRollupGenerator.ts | 56 +++++----- .../api-extractor-scenarios.api.json | 25 ++++- .../api-extractor-scenarios.api.ts | 4 + .../test-outputs/exportDuplicate1/rollup.d.ts | 10 +- .../exportImportedExternal/rollup.d.ts | 8 +- .../etc/test-outputs/exportStar2/rollup.d.ts | 8 +- .../src/exportDuplicate1/index.ts | 12 +-- .../dist/beta/api-extractor-test-02.d.ts | 2 +- .../dist/internal/api-extractor-test-02.d.ts | 2 +- .../dist/public/api-extractor-test-02.d.ts | 2 +- .../etc/api-extractor-test-02.api.ts | 9 +- 14 files changed, 200 insertions(+), 129 deletions(-) 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/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 0c4e103d8c2..217caf978c0 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -248,27 +248,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 +284,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); } } diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index d194831e6e3..c5ecd6098fa 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 emitWithExportKeyword(): 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/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index 77feaf97f79..8ab387f8331 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -95,13 +95,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,22 +102,33 @@ 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.emitWithExportKeyword) { + 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} */`); } } } @@ -178,12 +182,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.emitWithExportKeyword) { + replacedModifiers = 'export ' + replacedModifiers; } if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { @@ -219,12 +219,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.emitWithExportKeyword) { + span.modification.prefix = 'export ' + span.modification.prefix; } const declarationMetadata: DeclarationMetadata = collector.fetchMetadata(astDeclaration); 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/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/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportStar2/rollup.d.ts index 0433940f57e..261f6bc415b 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 @@ -1,14 +1,14 @@ import { Lib1Class } from 'api-extractor-lib1-test'; -export { Lib1Class }; import { Lib1Interface } from 'api-extractor-lib1-test'; -export { Lib1Interface }; import { Lib2Class } from 'api-extractor-lib2-test'; -export { Lib2Class }; import { Lib2Interface } from 'api-extractor-lib2-test'; -export { Lib2Interface }; /** @public */ export declare class A { } +export { Lib1Class } +export { Lib1Interface } +export { Lib2Class } +export { Lib2Interface } 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-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 Date: Mon, 14 Jan 2019 08:55:02 -0800 Subject: [PATCH 06/26] Improve exportStar3 test cases --- .../api-extractor-scenarios.api.json | 23 +++++++++++++++++++ .../api-extractor-scenarios.api.ts | 4 ++++ .../etc/test-outputs/exportStar3/rollup.d.ts | 4 ++++ .../src/exportStar3/index.ts | 2 +- .../src/exportStar3/reexportStar.ts | 1 + 5 files changed, 33 insertions(+), 1 deletion(-) 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..98a44347782 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 @@ -59,6 +59,29 @@ "name": "Lib1Class", "members": [], "implementsTokenRanges": [] + }, + { + "kind": "Interface", + "canonicalReference": "(RenamedLib2Interface:interface)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare interface " + }, + { + "kind": "Reference", + "text": "Lib2Interface" + }, + { + "kind": "Content", + "text": " " + } + ], + "releaseTag": "Public", + "name": "RenamedLib2Interface", + "members": [], + "extendsTokenRanges": [] } ] } 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..209117a71ce 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 @@ -6,5 +6,9 @@ declare class A { declare class Lib1Class { } +// @public (undocumented) +declare interface RenamedLib2Interface { +} + // (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..5748c53d899 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 @@ -7,4 +7,8 @@ export declare class A { export declare class Lib1Class { } +/** @public */ +export declare interface RenamedLib2Interface { +} + export { } 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 { } From eb7a2cc9427a15654a85bf9ad0a35aae57d1a0fe Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 01:18:04 -0800 Subject: [PATCH 07/26] Redesign the fundamental symbol following algorithm to solve exportStar3 --- .../src/analyzer/AstEntryPoint.ts | 29 -- apps/api-extractor/src/analyzer/AstImport.ts | 12 +- apps/api-extractor/src/analyzer/AstModule.ts | 33 +++ apps/api-extractor/src/analyzer/AstSymbol.ts | 4 +- .../src/analyzer/AstSymbolTable.ts | 272 +++++++++++------- .../src/analyzer/SymbolAnalyzer.ts | 119 +------- .../src/analyzer/TypeScriptHelpers.ts | 2 +- apps/api-extractor/src/collector/Collector.ts | 13 +- .../api-extractor-scenarios.api.json | 46 --- .../api-extractor-scenarios.api.ts | 10 +- .../etc/test-outputs/exportStar3/rollup.d.ts | 12 +- 11 files changed, 239 insertions(+), 313 deletions(-) delete mode 100644 apps/api-extractor/src/analyzer/AstEntryPoint.ts create mode 100644 apps/api-extractor/src/analyzer/AstModule.ts 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..1cda7b18e45 --- /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 starExportedModules: 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.starExportedModules = 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..01731320592 100644 --- a/apps/api-extractor/src/analyzer/AstSymbol.ts +++ b/apps/api-extractor/src/analyzer/AstSymbol.ts @@ -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; diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index d2839b968c9..abba9a82dc6 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -7,18 +7,18 @@ import * as ts from 'typescript'; import { PackageJsonLookup, InternalError } from '@microsoft/node-core-library'; import { AstDeclaration } from './AstDeclaration'; -import { SymbolAnalyzer, IFollowAliasesResult } from './SymbolAnalyzer'; +import { SymbolAnalyzer } 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'; /** * 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.) */ @@ -52,8 +52,8 @@ export class AstSymbolTable { /** * Cache of fetchEntryPoint() results. */ - private readonly _astEntryPointsBySourceFile: Map - = new Map(); + private readonly _astModulesBySourceFile: Map + = new Map(); public constructor(program: ts.Program, typeChecker: ts.TypeChecker, packageJsonLookup: PackageJsonLookup, logger: ILogger) { @@ -64,91 +64,186 @@ export class AstSymbolTable { } /** - * 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); + 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" + if (moduleSpecifier !== undefined && !ts.isExternalModuleNameRelative(moduleSpecifier)) { + // Yes, this is the entry point for an external package. + astModule.externalModulePath = moduleSpecifier; + + for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) { + + const astImportOptions: IAstImportOptions = { + exportName: exportedSymbol.name, + modulePath: astModule.externalModulePath + }; + + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); + const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(followedSymbol, true, astImportOptions); + + if (!astSymbol) { + throw new Error('Unsupported export: ' + exportedSymbol.name); + } + + astModule.exportedSymbols.set(exportedSymbol.name, astSymbol); + } + } else { + + 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 { + this._collectExportForAstModule(astModule, exportedSymbol); + } + } + + } - if (!rootFileSymbol.declarations || !rootFileSymbol.declarations.length) { - throw new Error('Unable to find a root declaration for ' + sourceFile.fileName); } + } - const exportedMembers: IExportedMember[] = []; + return astModule; + } - const visitedModules: Set = new Set(); - this._collectExportsFromModule(exportedMembers, rootFileSymbol, visitedModules, undefined); + private _collectExportForAstModule(astModule: AstModule, exportedSymbol: ts.Symbol): void { + let current: ts.Symbol = exportedSymbol; + + 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 || []) { + 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()); + } - /* - const exportSymbols: ts.Symbol[] = this._typeChecker.getExportsOfModule(rootFileSymbol) || []; -- - for (const exportSymbol of exportSymbols) { - const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(exportSymbol, true); + const specifierAstModule: AstModule | undefined = this._fetchSpecifierAstModule(exportDeclaration); + + if (specifierAstModule !== undefined) { + const exportedAstSymbol: AstSymbol | undefined = this._getExportOfAstModule(exportName, specifierAstModule); + if (exportedAstSymbol !== undefined) { + astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); + return; + } + } - if (!astSymbol) { - throw new Error('Unsupported export: ' + exportSymbol.name); } + } - this.analyze(astSymbol); + if (!(current.flags & ts.SymbolFlags.Alias)) { + break; + } - exportedMembers.push({ name: exportSymbol.name, astSymbol: astSymbol }); + const currentAlias: ts.Symbol = TypeScriptHelpers.getImmediateAliasedSymbol(current, this._typeChecker); + // Stop if we reach the end of the chain + if (!currentAlias || currentAlias === current) { + break; } - */ - astEntryPoint = new AstEntryPoint({ exportedMembers }); - this._astEntryPointsBySourceFile.set(sourceFile, astEntryPoint); + current = currentAlias; } - return astEntryPoint; - } - private _collectExportsFromModule(exportedMembers: IExportedMember[], moduleSymbol: ts.Symbol, - visitedModules: Set, followedModulePath: string | undefined): void { + // Otherwise, assume it is a normal declaration + const fetchedAstSymbol: AstSymbol | undefined = this._fetchAstSymbol(current, true, undefined); + if (fetchedAstSymbol !== undefined) { + astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); + } + } - // 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'". - if (visitedModules.has(moduleSymbol)) { - return; + 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)}`); } - visitedModules.add(moduleSymbol); + return astSymbol; + } - if (moduleSymbol.exports) { + private _tryGetExportOfAstModule(exportName: string, astModule: AstModule, + visitedAstModules: Set): AstSymbol | undefined { - for (const exportedSymbol of moduleSymbol.exports.values() as IterableIterator) { - if (exportedSymbol.escapedName !== ts.InternalSymbolName.ExportStar) { - const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(exportedSymbol, true, followedModulePath); + if (visitedAstModules.has(astModule)) { + return undefined; + } + visitedAstModules.add(astModule); - if (!astSymbol) { - throw new Error('Unsupported export: ' + exportedSymbol.name); - } + let astSymbol: AstSymbol | undefined = astModule.exportedSymbols.get(exportName); + if (astSymbol !== undefined) { + return astSymbol; + } - this.analyze(astSymbol); + // Try each of the star imports + for (const starExportedModule of astModule.starExportedModules) { + astSymbol = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); + if (astSymbol !== undefined) { + return astSymbol; + } + } - exportedMembers.push({ name: exportedSymbol.name, astSymbol: astSymbol }); - } else { + return undefined; + } - // 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() || []) { - if (ts.isExportDeclaration(exportStarDeclaration)) { - this._collectExportsFromExportStar(exportedMembers, exportStarDeclaration, - visitedModules, followedModulePath); - } else { - // Ignore ExportDeclaration nodes that don't match the expected pattern - // TODO: Should we report a warning? - } - } + private _collectExportsFromExportStar(astModule: AstModule, exportStarDeclaration: ts.Declaration): void { + if (ts.isExportDeclaration(exportStarDeclaration)) { - } + const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(exportStarDeclaration); + if (starExportedModule !== undefined) { + astModule.starExportedModules.add(starExportedModule); } + } else { + // Ignore ExportDeclaration nodes that don't match the expected pattern + // TODO: Should we report a warning? } } - private _collectExportsFromExportStar(exportedMembers: IExportedMember[], exportStarDeclaration: ts.ExportDeclaration, - visitedModules: Set, followedModulePath: string | undefined): void { + private _fetchSpecifierAstModule(exportStarDeclaration: ts.ImportDeclaration | ts.ExportDeclaration): AstModule + | undefined { // The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point' const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(exportStarDeclaration); @@ -157,17 +252,6 @@ export class AstSymbolTable { return; } - // Are we leaving the main project for the first time? - if (followedModulePath === undefined) { - // Match: "@microsoft/sp-lodash-subset" or "lodash/has" - // but ignore: "../folder/LocalFile" - if (!ts.isExternalModuleNameRelative(moduleSpecifier)) { - // Yes, we are traversing into an external package. That becomes the followedModulePath for everything - // we encounter as we continue recursing - followedModulePath = moduleSpecifier; - } - } - const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.getResolvedModule( exportStarDeclaration.getSourceFile(), moduleSpecifier); @@ -186,8 +270,8 @@ export class AstSymbolTable { throw new InternalError('getSourceFile() failed to locate ' + JSON.stringify(resolvedModule.resolvedFileName)); } - const moduleSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(moduleSourceFile); - this._collectExportsFromModule(exportedMembers, moduleSymbol, visitedModules, followedModulePath); + const specifierAstModule: AstModule = this.fetchAstModuleBySourceFile(moduleSourceFile, moduleSpecifier); + return specifierAstModule; } /** @@ -197,7 +281,7 @@ export class AstSymbolTable { * 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.) @@ -243,7 +327,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); } /** @@ -296,7 +380,7 @@ 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._fetchAstSymbol(symbol, true, undefined); if (referencedAstSymbol) { governingAstDeclaration._notifyReferencedAstSymbol(referencedAstSymbol); } @@ -338,31 +422,19 @@ 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, - followedModulePath?: string): AstSymbol | undefined { - - const followAliasesResult: IFollowAliasesResult = SymbolAnalyzer.followAliases(symbol, this._typeChecker); - - if (followedModulePath !== undefined) { - // FIX THIS - const exportName: string = followAliasesResult.astImport ? followAliasesResult.astImport.exportName - : followAliasesResult.localName; - - (followAliasesResult as any).astImport = new AstImport({ exportName, - modulePath: followedModulePath}); - } - - const followedSymbol: ts.Symbol = followAliasesResult.followedSymbol; + private _fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, + astImportOptions: IAstImportOptions | undefined): AstSymbol | undefined { // Filter out symbols representing constructs that we don't care about if (followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient)) { return undefined; } - if (followAliasesResult.isAmbient) { + if (SymbolAnalyzer.isAmbient(followedSymbol, this._typeChecker)) { + // API Extractor doesn't analyze ambient declarations at all return undefined; } @@ -373,7 +445,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) { @@ -443,7 +515,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); @@ -452,8 +524,8 @@ export class AstSymbolTable { } astSymbol = new AstSymbol({ - localName: followAliasesResult.localName, - followedSymbol: followAliasesResult.followedSymbol, + localName: followedSymbol.name, + followedSymbol: followedSymbol, astImport: astImport, parentAstSymbol: parentAstSymbol, rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined, @@ -493,7 +565,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 diff --git a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts index f1c24ae0a77..b48ac6479d1 100644 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts @@ -6,32 +6,6 @@ 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. @@ -75,87 +49,35 @@ export class SymbolAnalyzer { 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; + public static isAmbient(symbol: ts.Symbol, typeChecker: ts.TypeChecker): boolean { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(symbol, typeChecker); - // 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) { + if (followedSymbol.declarations && followedSymbol.declarations.length > 0) { + const firstDeclaration: ts.Declaration = followedSymbol.declarations[0]; // 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); + = TypeScriptHelpers.findHighestParent(firstDeclaration, ts.SyntaxKind.ModuleDeclaration); if (highestModuleDeclaration) { if (highestModuleDeclaration.name.getText().trim() === 'global') { - insideDeclareGlobal = true; + 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). - if (!insideDeclareGlobal) { - const sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( - current.declarations[0], ts.SyntaxKind.SourceFile); - if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { - isAmbient = false; - } + const sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( + firstDeclaration, ts.SyntaxKind.SourceFile); + if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { + return false; } } - return { - followedSymbol: current, - localName: declarationName || current.name, - astImport: undefined, - isAmbient: isAmbient - }; + return true; } - /** - * Helper function for _followAliases(), for handling ts.ExportDeclaration patterns - */ + /* private static _followAliasesForExportDeclaration(declaration: ts.Declaration, symbol: ts.Symbol, typeChecker: ts.TypeChecker): IFollowAliasesResult | undefined { @@ -206,9 +128,6 @@ export class SymbolAnalyzer { 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 { @@ -295,18 +214,6 @@ export class SymbolAnalyzer { return undefined; } +*/ - private static _tryGetExternalModulePath(declarationWithModuleSpecifier: ts.ImportDeclaration - | ts.ExportDeclaration): string | undefined { - - const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(declarationWithModuleSpecifier); - - // Match: "@microsoft/sp-lodash-subset" or "lodash/has" - // but ignore: "../folder/LocalFile" - if (moduleSpecifier && !ts.isExternalModuleNameRelative(moduleSpecifier)) { - return moduleSpecifier; - } - - return undefined; - } } diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index 3a360280a0d..337740421a6 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -8,7 +8,7 @@ import { TypeScriptMessageFormatter } from './TypeScriptMessageFormatter'; 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: * diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 217caf978c0..ffba321f55a 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'; @@ -81,7 +81,7 @@ 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(); @@ -167,7 +167,8 @@ export class Collector { } // Build the entry point - const astEntryPoint: AstEntryPoint = this.astSymbolTable.fetchEntryPoint(this.package.entryPointSourceFile); + const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleBySourceFile( + this.package.entryPointSourceFile, undefined); const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( this.package.entryPointSourceFile, this); @@ -183,10 +184,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); } 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 98a44347782..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,52 +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": "(RenamedLib2Interface:interface)", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export declare interface " - }, - { - "kind": "Reference", - "text": "Lib2Interface" - }, - { - "kind": "Content", - "text": " " - } - ], - "releaseTag": "Public", - "name": "RenamedLib2Interface", - "members": [], - "extendsTokenRanges": [] } ] } 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 209117a71ce..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,13 +2,7 @@ declare class A { } -// @public (undocumented) -declare class Lib1Class { -} - -// @public (undocumented) -declare interface RenamedLib2Interface { -} - +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 5748c53d899..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,14 +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 { -} - -/** @public */ -export declare interface RenamedLib2Interface { -} +export { Lib1Class } +export { RenamedLib2Interface } export { } From 0a472c3f82e271d03fee74e41349a3ba62ce73c6 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 01:41:52 -0800 Subject: [PATCH 08/26] Move the symbol following code into a separate class ExportAnalyzer.ts --- .../src/analyzer/AstSymbolTable.ts | 223 +--------------- .../src/analyzer/ExportAnalyzer.ts | 237 ++++++++++++++++++ .../src/analyzer/SymbolAnalyzer.ts | 30 --- .../src/analyzer/TypeScriptHelpers.ts | 31 +++ apps/api-extractor/src/collector/Collector.ts | 4 +- 5 files changed, 279 insertions(+), 246 deletions(-) create mode 100644 apps/api-extractor/src/analyzer/ExportAnalyzer.ts diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index abba9a82dc6..1c9b6e67d4f 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -1,8 +1,6 @@ // 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'; @@ -14,6 +12,7 @@ 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. @@ -26,6 +25,7 @@ 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,229 +49,23 @@ export class AstSymbolTable { */ private readonly _astSymbolsByImportKey: Map = new Map(); - /** - * Cache of fetchEntryPoint() results. - */ - private readonly _astModulesBySourceFile: 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); + this._exportAnalyzer.fetchAstSymbol = this._fetchAstSymbol; } /** * 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" - if (moduleSpecifier !== undefined && !ts.isExternalModuleNameRelative(moduleSpecifier)) { - // Yes, this is the entry point for an external package. - astModule.externalModulePath = moduleSpecifier; - - for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) { - - const astImportOptions: IAstImportOptions = { - exportName: exportedSymbol.name, - modulePath: astModule.externalModulePath - }; - - const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); - const astSymbol: AstSymbol | undefined = this._fetchAstSymbol(followedSymbol, true, astImportOptions); - - if (!astSymbol) { - throw new Error('Unsupported export: ' + exportedSymbol.name); - } - - astModule.exportedSymbols.set(exportedSymbol.name, astSymbol); - } - } else { - - 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 { - this._collectExportForAstModule(astModule, exportedSymbol); - } - } - - } - - } - } - - return astModule; - } - - private _collectExportForAstModule(astModule: AstModule, exportedSymbol: ts.Symbol): void { - let current: ts.Symbol = exportedSymbol; - - 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 || []) { - 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()); - } - - const specifierAstModule: AstModule | undefined = this._fetchSpecifierAstModule(exportDeclaration); - - if (specifierAstModule !== undefined) { - const exportedAstSymbol: AstSymbol | undefined = this._getExportOfAstModule(exportName, specifierAstModule); - if (exportedAstSymbol !== undefined) { - astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); - return; - } - } - - } - } - - if (!(current.flags & ts.SymbolFlags.Alias)) { - break; - } - - const currentAlias: ts.Symbol = TypeScriptHelpers.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 - const fetchedAstSymbol: AstSymbol | undefined = this._fetchAstSymbol(current, true, undefined); - if (fetchedAstSymbol !== undefined) { - astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); - } - } - - 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.starExportedModules) { - astSymbol = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); - if (astSymbol !== undefined) { - return astSymbol; - } - } - - return undefined; - } - - private _collectExportsFromExportStar(astModule: AstModule, exportStarDeclaration: ts.Declaration): void { - if (ts.isExportDeclaration(exportStarDeclaration)) { - - const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(exportStarDeclaration); - if (starExportedModule !== undefined) { - astModule.starExportedModules.add(starExportedModule); - } - - } else { - // Ignore ExportDeclaration nodes that don't match the expected pattern - // TODO: Should we report a warning? - } - } - - private _fetchSpecifierAstModule(exportStarDeclaration: ts.ImportDeclaration | ts.ExportDeclaration): AstModule - | undefined { - - // 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) { - // TODO: Should we report a warning? - return; - } - - const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.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; + public fetchEntryPointModule(sourceFile: ts.SourceFile): AstModule { + return this._exportAnalyzer.fetchAstModuleBySourceFile(sourceFile, undefined); } /** @@ -429,11 +223,12 @@ export class AstSymbolTable { astImportOptions: IAstImportOptions | undefined): 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 (SymbolAnalyzer.isAmbient(followedSymbol, this._typeChecker)) { + if (TypeScriptHelpers.isAmbient(followedSymbol, this._typeChecker)) { // API Extractor doesn't analyze ambient declarations at all return undefined; } diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts new file mode 100644 index 00000000000..6f3a4b16288 --- /dev/null +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -0,0 +1,237 @@ +// 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'; + +export class ExportAnalyzer { + public fetchAstSymbol: (followedSymbol: ts.Symbol, addIfMissing: boolean, + astImportOptions: IAstImportOptions | undefined) => AstSymbol | undefined; + + private readonly _program: ts.Program; + private readonly _typeChecker: ts.TypeChecker; + + private readonly _astModulesBySourceFile: Map + = new Map(); + + public constructor(program: ts.Program, typeChecker: ts.TypeChecker) { + this._program = program; + this._typeChecker = typeChecker; + } + + /** + * 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" + if (moduleSpecifier !== undefined && !ts.isExternalModuleNameRelative(moduleSpecifier)) { + // Yes, this is the entry point for an external package. + astModule.externalModulePath = moduleSpecifier; + + for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) { + + const astImportOptions: IAstImportOptions = { + exportName: exportedSymbol.name, + modulePath: astModule.externalModulePath + }; + + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); + const astSymbol: AstSymbol | undefined = this.fetchAstSymbol(followedSymbol, true, astImportOptions); + + if (!astSymbol) { + throw new Error('Unsupported export: ' + exportedSymbol.name); + } + + astModule.exportedSymbols.set(exportedSymbol.name, astSymbol); + } + } else { + + 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 { + this._collectExportForAstModule(astModule, exportedSymbol); + } + } + + } + + } + } + + return astModule; + } + + private _collectExportForAstModule(astModule: AstModule, exportedSymbol: ts.Symbol): void { + let current: ts.Symbol = exportedSymbol; + + 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 || []) { + 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()); + } + + const specifierAstModule: AstModule | undefined = this._fetchSpecifierAstModule(exportDeclaration); + + if (specifierAstModule !== undefined) { + const exportedAstSymbol: AstSymbol | undefined = this._getExportOfAstModule(exportName, specifierAstModule); + if (exportedAstSymbol !== undefined) { + astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); + return; + } + } + + } + } + + if (!(current.flags & ts.SymbolFlags.Alias)) { // tslint:disable-line:no-bitwise + break; + } + + const currentAlias: ts.Symbol = TypeScriptHelpers.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 + const fetchedAstSymbol: AstSymbol | undefined = this.fetchAstSymbol(current, true, undefined); + if (fetchedAstSymbol !== undefined) { + astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); + } + } + + 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.starExportedModules) { + astSymbol = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); + if (astSymbol !== undefined) { + return astSymbol; + } + } + + return undefined; + } + + private _collectExportsFromExportStar(astModule: AstModule, exportStarDeclaration: ts.Declaration): void { + if (ts.isExportDeclaration(exportStarDeclaration)) { + + const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(exportStarDeclaration); + if (starExportedModule !== undefined) { + astModule.starExportedModules.add(starExportedModule); + } + + } else { + // Ignore ExportDeclaration nodes that don't match the expected pattern + // TODO: Should we report a warning? + } + } + + private _fetchSpecifierAstModule(exportStarDeclaration: ts.ImportDeclaration | ts.ExportDeclaration): AstModule + | undefined { + + // 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) { + // TODO: Should we report a warning? + return; + } + + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.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 index b48ac6479d1..1dcfd03ea91 100644 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts @@ -5,8 +5,6 @@ import * as ts from 'typescript'; -import { TypeScriptHelpers } from './TypeScriptHelpers'; - /** * This is a helper class for DtsRollupGenerator and AstSymbolTable. * Its main role is to provide an expanded version of TypeScriptHelpers.followAliases() @@ -49,34 +47,6 @@ export class SymbolAnalyzer { return false; } - 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 sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( - firstDeclaration, ts.SyntaxKind.SourceFile); - if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { - return false; - } - } - - return true; - } - /* private static _followAliasesForExportDeclaration(declaration: ts.Declaration, symbol: ts.Symbol, typeChecker: ts.TypeChecker): IFollowAliasesResult | undefined { diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index 337740421a6..f12885b9553 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -38,6 +38,37 @@ export class TypeScriptHelpers { return (typeChecker as any).getImmediateAliasedSymbol(symbol); // tslint:disable-line:no-any } + /** + * Returns true if the specified symbol is an ambient declaration. + */ + 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 sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( + firstDeclaration, ts.SyntaxKind.SourceFile); + if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { + return false; + } + } + + return true; + } + /** * 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 diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index ffba321f55a..16b30fbe13f 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -167,8 +167,8 @@ export class Collector { } // Build the entry point - const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleBySourceFile( - this.package.entryPointSourceFile, undefined); + const astEntryPoint: AstModule = this.astSymbolTable.fetchEntryPointModule( + this.package.entryPointSourceFile); const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( this.package.entryPointSourceFile, this); From e04e4343c7a8dfb01cbf61cfc706bde2ca568a9c Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 02:03:01 -0800 Subject: [PATCH 09/26] Generate all config files even if some of the tests failed --- build-tests/api-extractor-scenarios/src/runScenarios.ts | 5 +++++ 1 file changed, 5 insertions(+) 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]); } + } From 29be5e0216e34fe0f1c293df90a845b6df9e86c1 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 16:10:50 -0800 Subject: [PATCH 10/26] Reintroduce analysis of ImportSpecifier and ImportClause and fix some bugs --- apps/api-extractor/src/analyzer/AstModule.ts | 4 +- .../src/analyzer/AstSymbolTable.ts | 10 +- .../src/analyzer/ExportAnalyzer.ts | 213 +++++++++++++----- 3 files changed, 172 insertions(+), 55 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstModule.ts b/apps/api-extractor/src/analyzer/AstModule.ts index 1cda7b18e45..511f0aace82 100644 --- a/apps/api-extractor/src/analyzer/AstModule.ts +++ b/apps/api-extractor/src/analyzer/AstModule.ts @@ -11,7 +11,7 @@ export class AstModule { public readonly sourceFile: ts.SourceFile; public readonly exportedSymbols: Map; - public readonly starExportedModules: Set; + public readonly starExportedExternalModules: Set; /** * Example: "@microsoft/node-core-library/lib/FileSystem" @@ -22,7 +22,7 @@ export class AstModule { public constructor(sourceFile: ts.SourceFile) { this.sourceFile = sourceFile; this.exportedSymbols = new Map(); - this.starExportedModules = new Set(); + this.starExportedExternalModules = new Set(); this.externalModulePath = undefined; } diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 1c9b6e67d4f..98e9c1d3fd8 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -56,8 +56,14 @@ export class AstSymbolTable { this._typeChecker = typeChecker; this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, logger); - this._exportAnalyzer = new ExportAnalyzer(this._program, this._typeChecker); - this._exportAnalyzer.fetchAstSymbol = this._fetchAstSymbol; + this._exportAnalyzer = new ExportAnalyzer( + this._program, + this._typeChecker, + { + analyze: this.analyze.bind(this), + fetchAstSymbol: this._fetchAstSymbol.bind(this) + } + ); } /** diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 6f3a4b16288..d73a8f996b0 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -9,19 +9,25 @@ import { AstSymbol } from './AstSymbol'; import { IAstImportOptions } from './AstImport'; import { AstModule } from './AstModule'; -export class ExportAnalyzer { - public fetchAstSymbol: (followedSymbol: ts.Symbol, addIfMissing: boolean, - astImportOptions: IAstImportOptions | undefined) => AstSymbol | undefined; +export interface IAstSymbolTable { + fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, + astImportOptions: IAstImportOptions | undefined): AstSymbol | undefined; + + analyze(astSymbol: AstSymbol): void; +} +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) { + public constructor(program: ts.Program, typeChecker: ts.TypeChecker, astSymbolTable: IAstSymbolTable) { this._program = program; this._typeChecker = typeChecker; + this._astSymbolTable = astSymbolTable; } /** @@ -54,7 +60,8 @@ export class ExportAnalyzer { }; const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); - const astSymbol: AstSymbol | undefined = this.fetchAstSymbol(followedSymbol, true, astImportOptions); + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol( + followedSymbol, true, astImportOptions); if (!astSymbol) { throw new Error('Unsupported export: ' + exportedSymbol.name); @@ -94,45 +101,11 @@ export class ExportAnalyzer { // Is this symbol an import/export that we need to follow to find the real declaration? for (const declaration of current.declarations || []) { - 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()); - } - - const specifierAstModule: AstModule | undefined = this._fetchSpecifierAstModule(exportDeclaration); - - if (specifierAstModule !== undefined) { - const exportedAstSymbol: AstSymbol | undefined = this._getExportOfAstModule(exportName, specifierAstModule); - if (exportedAstSymbol !== undefined) { - astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); - return; - } - } - + if (this._matchExportDeclaration(astModule, exportedSymbol, declaration)) { + return; + } + if (this._matchImportDeclaration(astModule, exportedSymbol, declaration)) { + return; } } @@ -150,12 +123,141 @@ export class ExportAnalyzer { } // Otherwise, assume it is a normal declaration - const fetchedAstSymbol: AstSymbol | undefined = this.fetchAstSymbol(current, true, undefined); + const fetchedAstSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol(current, true, undefined); if (fetchedAstSymbol !== undefined) { + this._astSymbolTable.analyze(fetchedAstSymbol); astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); } } + private _matchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + declaration: ts.Declaration): boolean { + + 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 exportedAstSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); + + astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); + + return true; + } + } + + return false; + } + + private _matchImportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + declaration: ts.Declaration): boolean { + + const importDeclaration: ts.ImportDeclaration | undefined + = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ImportDeclaration); + + if (importDeclaration) { + let exportName: string; + + 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=[;] + + throw new InternalError('"import * as x" is not supported yet'); + } + + 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.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 InternalError('Unimplemented import declaration kind: ' + declaration.getText()); + } + + const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importDeclaration); + const exportedAstSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); + + astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); + } + + return false; + } + private _getExportOfAstModule(exportName: string, astModule: AstModule): AstSymbol { const visitedAstModules: Set = new Set(); const astSymbol: AstSymbol | undefined = this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules); @@ -179,7 +281,7 @@ export class ExportAnalyzer { } // Try each of the star imports - for (const starExportedModule of astModule.starExportedModules) { + for (const starExportedModule of astModule.starExportedExternalModules) { astSymbol = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); if (astSymbol !== undefined) { return astSymbol; @@ -194,7 +296,18 @@ export class ExportAnalyzer { const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(exportStarDeclaration); if (starExportedModule !== undefined) { - astModule.starExportedModules.add(starExportedModule); + if (starExportedModule.isExternal) { + astModule.starExportedExternalModules.add(starExportedModule); + } else { + for (const [exportName, exportedSymbol] of starExportedModule.exportedSymbols) { + if (!astModule.exportedSymbols.has(exportName)) { + astModule.exportedSymbols.set(exportName, exportedSymbol); + } + } + for (const starExportedExternalModule of starExportedModule.starExportedExternalModules) { + astModule.starExportedExternalModules.add(starExportedExternalModule); + } + } } } else { @@ -203,14 +316,12 @@ export class ExportAnalyzer { } } - private _fetchSpecifierAstModule(exportStarDeclaration: ts.ImportDeclaration | ts.ExportDeclaration): AstModule - | undefined { + 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) { - // TODO: Should we report a warning? - return; + throw new InternalError('Unable to parse module specifier'); } const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.getResolvedModule( From ff512cb986b5c05a45e41190c17ebee9379893d3 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 17:17:02 -0800 Subject: [PATCH 11/26] Eliminate the old SymbolAnalyzer class --- .../src/analyzer/AstDeclaration.ts | 38 +++- .../src/analyzer/AstSymbolTable.ts | 7 +- .../src/analyzer/SymbolAnalyzer.ts | 189 ------------------ .../src/generators/DtsRollupGenerator.ts | 3 +- .../src/generators/ReviewFileGenerator.ts | 5 +- 5 files changed, 43 insertions(+), 199 deletions(-) delete mode 100644 apps/api-extractor/src/analyzer/SymbolAnalyzer.ts diff --git a/apps/api-extractor/src/analyzer/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts index 12f0bfbc8c6..99db3d2e2dd 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 { @@ -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" + // 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/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 98e9c1d3fd8..8e8bcf83fd3 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -5,7 +5,6 @@ import * as ts from 'typescript'; import { PackageJsonLookup, InternalError } from '@microsoft/node-core-library'; import { AstDeclaration } from './AstDeclaration'; -import { SymbolAnalyzer } from './SymbolAnalyzer'; import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; import { AstImport, IAstImportOptions } from './AstImport'; @@ -213,7 +212,7 @@ export class AstSymbolTable { } private _fetchAstSymbolForNode(node: ts.Node): AstSymbol | undefined { - if (!SymbolAnalyzer.isAstDeclaration(node.kind)) { + if (!AstDeclaration.isSupportedSyntaxKind(node.kind)) { return undefined; } @@ -291,7 +290,7 @@ export class AstSymbolTable { if (!nominal) { 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`); } @@ -387,7 +386,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/SymbolAnalyzer.ts b/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts deleted file mode 100644 index 1dcfd03ea91..00000000000 --- a/apps/api-extractor/src/analyzer/SymbolAnalyzer.ts +++ /dev/null @@ -1,189 +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'; - -/** - * 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; - } - - /* - 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) { - const externalModulePath: string | undefined = SymbolAnalyzer._tryGetExternalModulePath(exportDeclaration); - - if (externalModulePath) { - return { - followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), - localName: exportName, - astImport: new AstImport({ modulePath: externalModulePath, exportName }), - isAmbient: false - }; - } - } - - } - - return undefined; - } - - 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) { - const externalModulePath: string | undefined = SymbolAnalyzer._tryGetExternalModulePath(importDeclaration); - if (externalModulePath) { - return { - followedSymbol: TypeScriptHelpers.followAliases(symbol, typeChecker), - localName: symbol.name, - astImport: new AstImport({ modulePath: externalModulePath, exportName }), - isAmbient: false - }; - } - } - - } - - return undefined; - } -*/ - -} diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index 8ab387f8331..855fcb55ac1 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'; /** @@ -273,7 +272,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 43bd0f6f777..e387c1973ca 100644 --- a/apps/api-extractor/src/generators/ReviewFileGenerator.ts +++ b/apps/api-extractor/src/generators/ReviewFileGenerator.ts @@ -9,7 +9,6 @@ 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'; @@ -102,7 +101,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; @@ -170,7 +169,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) { From e8842ddff160d6b42afaba34930f254c5c8914bb Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 18:31:17 -0800 Subject: [PATCH 12/26] Emit "export * from" in .d.ts rollup and .api.ts --- apps/api-extractor/src/collector/Collector.ts | 18 ++++++++++++++++++ .../src/generators/DtsRollupGenerator.ts | 7 +++++++ .../src/generators/ReviewFileGenerator.ts | 7 +++++++ .../exportStar2/api-extractor-scenarios.api.ts | 7 +++---- .../etc/test-outputs/exportStar2/rollup.d.ts | 11 +++-------- 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 16b30fbe13f..862405f07e6 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -87,6 +87,8 @@ export class Collector { 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 +152,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. */ @@ -169,6 +179,7 @@ export class Collector { // Build the entry point const astEntryPoint: AstModule = this.astSymbolTable.fetchEntryPointModule( this.package.entryPointSourceFile); + this._astEntryPoint = astEntryPoint; const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( this.package.entryPointSourceFile, this); @@ -202,9 +213,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 { diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index 855fcb55ac1..cd734c08bda 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -132,6 +132,13 @@ export class DtsRollupGenerator { } } + 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(); diff --git a/apps/api-extractor/src/generators/ReviewFileGenerator.ts b/apps/api-extractor/src/generators/ReviewFileGenerator.ts index e387c1973ca..5a4b5cec788 100644 --- a/apps/api-extractor/src/generators/ReviewFileGenerator.ts +++ b/apps/api-extractor/src/generators/ReviewFileGenerator.ts @@ -64,6 +64,13 @@ export class ReviewFileGenerator { } } + 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)'); 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 ad598f92b8f..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,9 +2,8 @@ declare class A { } -export { Lib1Class } from 'api-extractor-lib1-test'; -export { Lib1Interface } from 'api-extractor-lib1-test'; -export { Lib2Class } from 'api-extractor-lib2-test'; -export { Lib2Interface } from 'api-extractor-lib2-test'; + +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 261f6bc415b..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 @@ -1,14 +1,9 @@ -import { Lib1Class } from 'api-extractor-lib1-test'; -import { Lib1Interface } from 'api-extractor-lib1-test'; -import { Lib2Class } from 'api-extractor-lib2-test'; -import { Lib2Interface } from 'api-extractor-lib2-test'; /** @public */ export declare class A { } -export { Lib1Class } -export { Lib1Interface } -export { Lib2Class } -export { Lib2Interface } + +export * from "api-extractor-lib1-test"; +export * from "api-extractor-lib2-test"; export { } From b76720a57c00c581ae2586da19423216dfa74a8b Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 18:42:04 -0800 Subject: [PATCH 13/26] Reintroduce the declarationName logic --- apps/api-extractor/src/analyzer/AstSymbolTable.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 8e8bcf83fd3..8b923b86601 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -323,8 +323,20 @@ export class AstSymbolTable { } } + // 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". + let localName: string | undefined = 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: followedSymbol.name, + localName: localName, followedSymbol: followedSymbol, astImport: astImport, parentAstSymbol: parentAstSymbol, From 2a00e8ec1651f3115adaa85ad881cb3f8af277fb Mon Sep 17 00:00:00 2001 From: pgonzal Date: Tue, 15 Jan 2019 19:13:53 -0800 Subject: [PATCH 14/26] Implement AstSymboltable.fetchReferencedAstSymbol --- .../src/analyzer/AstSymbolTable.ts | 7 ++++- .../src/analyzer/ExportAnalyzer.ts | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 8b923b86601..c0da75dd298 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -73,6 +73,10 @@ export class AstSymbolTable { return this._exportAnalyzer.fetchAstModuleBySourceFile(sourceFile, undefined); } + public fetchReferencedAstSymbol(symbol: ts.Symbol, sourceFile: ts.SourceFile): AstSymbol | undefined { + return this._exportAnalyzer.fetchReferencedAstSymbol(symbol, sourceFile); + } + /** * Ensures that AstSymbol.analyzed is true for the provided symbol. The operation * starts from the root symbol and then fills out all children of all declarations, and @@ -179,7 +183,8 @@ export class AstSymbolTable { throw new Error('Symbol not found for identifier: ' + symbolNode.getText()); } - const referencedAstSymbol: AstSymbol | undefined = this._fetchAstSymbol(symbol, true, undefined); + const referencedAstSymbol: AstSymbol | undefined + = this.fetchReferencedAstSymbol(symbol, symbolNode.getSourceFile()); if (referencedAstSymbol) { governingAstDeclaration._notifyReferencedAstSymbol(referencedAstSymbol); } diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index d73a8f996b0..c37c719943e 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -82,7 +82,11 @@ export class ExportAnalyzer { } } else { - this._collectExportForAstModule(astModule, exportedSymbol); + const fetchedAstSymbol: AstSymbol | undefined = this._fetchAstSymbolFromModule(astModule, exportedSymbol); + if (fetchedAstSymbol !== undefined) { + this._astSymbolTable.analyze(fetchedAstSymbol); + astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); + } } } @@ -94,17 +98,26 @@ export class ExportAnalyzer { return astModule; } - private _collectExportForAstModule(astModule: AstModule, exportedSymbol: ts.Symbol): void { - let current: ts.Symbol = exportedSymbol; + 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 || []) { - if (this._matchExportDeclaration(astModule, exportedSymbol, declaration)) { + if (this._matchExportDeclaration(astModule, symbol, declaration)) { return; } - if (this._matchImportDeclaration(astModule, exportedSymbol, declaration)) { + if (this._matchImportDeclaration(astModule, symbol, declaration)) { return; } } @@ -123,11 +136,7 @@ export class ExportAnalyzer { } // Otherwise, assume it is a normal declaration - const fetchedAstSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol(current, true, undefined); - if (fetchedAstSymbol !== undefined) { - this._astSymbolTable.analyze(fetchedAstSymbol); - astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); - } + return this._astSymbolTable.fetchAstSymbol(current, true, undefined); } private _matchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, @@ -253,6 +262,7 @@ export class ExportAnalyzer { const exportedAstSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); + return true; } return false; From 3f36ff96483a7211ce090500b824f0dc7d7ad45c Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 11:17:37 -0800 Subject: [PATCH 15/26] Reintroduce limited support for ts.SyntaxKind.ExportSpecifier --- .../src/analyzer/AstSymbolTable.ts | 22 ++++--- .../src/analyzer/ExportAnalyzer.ts | 61 +++++++++++-------- .../src/analyzer/TypeScriptHelpers.ts | 6 +- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index c0da75dd298..3827121675f 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -230,7 +230,7 @@ export class AstSymbolTable { } private _fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, - astImportOptions: IAstImportOptions | undefined): AstSymbol | undefined { + astImportOptions: IAstImportOptions | undefined, localName?: string): AstSymbol | undefined { // Filter out symbols representing constructs that we don't care about // tslint:disable-next-line:no-bitwise @@ -328,15 +328,17 @@ export class AstSymbolTable { } } - // 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". - let localName: string | undefined = 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; + 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; + } } } diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index c37c719943e..657e2502394 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -11,7 +11,7 @@ import { AstModule } from './AstModule'; export interface IAstSymbolTable { fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, - astImportOptions: IAstImportOptions | undefined): AstSymbol | undefined; + astImportOptions: IAstImportOptions | undefined, localName?: string): AstSymbol | undefined; analyze(astSymbol: AstSymbol): void; } @@ -114,11 +114,14 @@ export class ExportAnalyzer { // Is this symbol an import/export that we need to follow to find the real declaration? for (const declaration of current.declarations || []) { - if (this._matchExportDeclaration(astModule, symbol, declaration)) { - return; + let matchedAstSymbol: AstSymbol | undefined; + matchedAstSymbol = this._matchExportDeclaration(astModule, symbol, declaration); + if (matchedAstSymbol !== undefined) { + return matchedAstSymbol; } - if (this._matchImportDeclaration(astModule, symbol, declaration)) { - return; + matchedAstSymbol = this._matchImportDeclaration(astModule, symbol, declaration); + if (matchedAstSymbol !== undefined) { + return matchedAstSymbol; } } @@ -140,7 +143,7 @@ export class ExportAnalyzer { } private _matchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, - declaration: ts.Declaration): boolean { + declaration: ts.Declaration): AstSymbol | undefined { const exportDeclaration: ts.ExportDeclaration | undefined = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ExportDeclaration); @@ -174,25 +177,22 @@ export class ExportAnalyzer { // Ignore "export { A }" without a module specifier if (exportDeclaration.moduleSpecifier) { const specifierAstModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration); - const exportedAstSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); - - astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); - - return true; + const astSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); + return astSymbol; } } - return false; + return undefined; } private _matchImportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, - declaration: ts.Declaration): boolean { + declaration: ts.Declaration): AstSymbol | undefined { const importDeclaration: ts.ImportDeclaration | undefined = TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ImportDeclaration); if (importDeclaration) { - let exportName: string; + const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importDeclaration); if (declaration.kind === ts.SyntaxKind.NamespaceImport) { // EXAMPLE: @@ -209,7 +209,23 @@ export class ExportAnalyzer { // StringLiteral: pre=['the-lib'] // SemicolonToken: pre=[;] - throw new InternalError('"import * as x" is not supported yet'); + 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) { @@ -234,7 +250,9 @@ export class ExportAnalyzer { // Example: " ExportName as RenamedName" const importSpecifier: ts.ImportSpecifier = declaration as ts.ImportSpecifier; - exportName = (importSpecifier.propertyName || importSpecifier.name).getText().trim(); + 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';" @@ -253,19 +271,14 @@ export class ExportAnalyzer { // FromKeyword: pre=[from] sep=[ ] // StringLiteral: pre=['./A'] // SemicolonToken: pre=[;] - exportName = ts.InternalSymbolName.Default; + const astSymbol: AstSymbol = this._getExportOfAstModule(ts.InternalSymbolName.Default, specifierAstModule); + return astSymbol; } else { throw new InternalError('Unimplemented import declaration kind: ' + declaration.getText()); } - - const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importDeclaration); - const exportedAstSymbol: AstSymbol = this._getExportOfAstModule(exportName, specifierAstModule); - - astModule.exportedSymbols.set(exportedSymbol.name, exportedAstSymbol); - return true; } - return false; + return undefined; } private _getExportOfAstModule(exportName: string, astModule: AstModule): AstSymbol { diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index f12885b9553..2890096574e 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -59,9 +59,9 @@ export class TypeScriptHelpers { // 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 sourceFileNode: ts.Node | undefined = TypeScriptHelpers.findFirstParent( - firstDeclaration, ts.SyntaxKind.SourceFile); - if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) { + const sourceFile: ts.SourceFile = firstDeclaration.getSourceFile(); + + if (!!typeChecker.getSymbolAtLocation(sourceFile)) { return false; } } From 19f0204f6cc163a22c1d745863cd6b9d205219e5 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 14:41:45 -0800 Subject: [PATCH 16/26] Clean up logic --- .../src/analyzer/ExportAnalyzer.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 657e2502394..367d1146f65 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -48,15 +48,22 @@ export class ExportAnalyzer { // 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)) { - // Yes, this is the entry point for an external package. + // 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: astModule.externalModulePath + modulePath: moduleSpecifier! }; const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); @@ -70,6 +77,7 @@ export class ExportAnalyzer { 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) { @@ -84,7 +92,6 @@ export class ExportAnalyzer { } else { const fetchedAstSymbol: AstSymbol | undefined = this._fetchAstSymbolFromModule(astModule, exportedSymbol); if (fetchedAstSymbol !== undefined) { - this._astSymbolTable.analyze(fetchedAstSymbol); astModule.exportedSymbols.set(exportedSymbol.name, fetchedAstSymbol); } } @@ -93,6 +100,12 @@ export class ExportAnalyzer { } } + + if (astModule.isExternal) { + for (const exportedAstSymbol of astModule.exportedSymbols.values()) { + this._astSymbolTable.analyze(exportedAstSymbol); + } + } } return astModule; From 1118a37448b98fe966674a429529f88fa98d3fe0 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 15:32:09 -0800 Subject: [PATCH 17/26] Rename "nominal" to "nominalAnalysis" to avoid confusion with nominal types --- apps/api-extractor/src/analyzer/AstDeclaration.ts | 4 ++-- apps/api-extractor/src/analyzer/AstSymbol.ts | 6 +++--- apps/api-extractor/src/analyzer/AstSymbolTable.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts index 99db3d2e2dd..1b13868ef7b 100644 --- a/apps/api-extractor/src/analyzer/AstDeclaration.ts +++ b/apps/api-extractor/src/analyzer/AstDeclaration.ts @@ -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'; @@ -206,7 +206,7 @@ export class AstDeclaration { case ts.SyntaxKind.VariableDeclaration: return true; - // NOTE: In contexts where a source file is treated as a module, we do create "nominal" + // 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 diff --git a/apps/api-extractor/src/analyzer/AstSymbol.ts b/apps/api-extractor/src/analyzer/AstSymbol.ts index 01731320592..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; } @@ -57,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. @@ -92,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 3827121675f..c82e1a1e201 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -94,7 +94,7 @@ export class AstSymbolTable { return; } - if (astSymbol.nominal) { + if (astSymbol.nominalAnalysis) { // We don't analyze nominal symbols astSymbol._notifyAnalyzed(); return; @@ -265,7 +265,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: @@ -279,7 +279,7 @@ 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 @@ -287,13 +287,13 @@ export class AstSymbolTable { const followedSymbolSourceFile: ts.SourceFile = followedSymbol.declarations[0].getSourceFile(); 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 (!AstDeclaration.isSupportedSyntaxKind(declaration.kind)) { throw new InternalError(`The "${followedSymbol.name}" symbol uses the construct` @@ -348,7 +348,7 @@ export class AstSymbolTable { astImport: astImport, parentAstSymbol: parentAstSymbol, rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined, - nominal: nominal + nominalAnalysis: nominalAnalysis }); this._astSymbolsBySymbol.set(followedSymbol, astSymbol); From 9c39f6127f5964ac943a9e47ae711c0aa0abd374 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 15:34:28 -0800 Subject: [PATCH 18/26] Rename emitWithExportKeyword to shouldInlineExport --- apps/api-extractor/src/collector/CollectorEntity.ts | 2 +- apps/api-extractor/src/generators/DtsRollupGenerator.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index c5ecd6098fa..d49cafb9910 100644 --- a/apps/api-extractor/src/collector/CollectorEntity.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -78,7 +78,7 @@ export class CollectorEntity { * 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 emitWithExportKeyword(): boolean { + public get shouldInlineExport(): boolean { return this._singleExportName !== undefined && this._singleExportName !== ts.InternalSymbolName.Default && this.astSymbol.astImport === undefined; diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index cd734c08bda..451fa38f030 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -119,7 +119,7 @@ export class DtsRollupGenerator { } } - if (!entity.emitWithExportKeyword) { + if (!entity.shouldInlineExport) { for (const exportName of entity.exportNames) { if (exportName === ts.InternalSymbolName.Default) { indentedWriter.writeLine(`export default ${entity.nameForEmit};`); @@ -188,7 +188,7 @@ export class DtsRollupGenerator { replacedModifiers += 'declare '; } - if (entity.emitWithExportKeyword) { + if (entity.shouldInlineExport) { replacedModifiers = 'export ' + replacedModifiers; } @@ -225,7 +225,7 @@ export class DtsRollupGenerator { span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; span.modification.suffix = ';'; - if (entity.emitWithExportKeyword) { + if (entity.shouldInlineExport) { span.modification.prefix = 'export ' + span.modification.prefix; } From 1afcdbdba71ef2f91ded58c861bef46765c13956 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 15:43:17 -0800 Subject: [PATCH 19/26] Split compiler internals into a separate file --- .../src/analyzer/ExportAnalyzer.ts | 5 +- .../src/analyzer/TypeScriptHelpers.ts | 60 +----------------- .../src/analyzer/TypeScriptInternals.ts | 61 +++++++++++++++++++ apps/api-extractor/src/collector/Collector.ts | 3 +- 4 files changed, 69 insertions(+), 60 deletions(-) create mode 100644 apps/api-extractor/src/analyzer/TypeScriptInternals.ts diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 367d1146f65..77c06fa0db4 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -8,6 +8,7 @@ import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; import { IAstImportOptions } from './AstImport'; import { AstModule } from './AstModule'; +import { TypeScriptInternals } from './TypeScriptInternals'; export interface IAstSymbolTable { fetchAstSymbol(followedSymbol: ts.Symbol, addIfMissing: boolean, @@ -142,7 +143,7 @@ export class ExportAnalyzer { break; } - const currentAlias: ts.Symbol = TypeScriptHelpers.getImmediateAliasedSymbol(current, this._typeChecker); + const currentAlias: ts.Symbol = TypeScriptInternals.getImmediateAliasedSymbol(current, this._typeChecker); // Stop if we reach the end of the chain if (!currentAlias || currentAlias === current) { break; @@ -360,7 +361,7 @@ export class ExportAnalyzer { throw new InternalError('Unable to parse module specifier'); } - const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptHelpers.getResolvedModule( + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( exportStarDeclaration.getSourceFile(), moduleSpecifier); if (resolvedModule === undefined) { diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index 2890096574e..29d8710a86f 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -5,6 +5,7 @@ import * as ts from 'typescript'; import { TypeScriptMessageFormatter } from './TypeScriptMessageFormatter'; +import { TypeScriptInternals } from './TypeScriptInternals'; export class TypeScriptHelpers { /** @@ -34,10 +35,6 @@ 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 true if the specified symbol is an ambient declaration. */ @@ -69,27 +66,12 @@ export class TypeScriptHelpers { return true; } - /** - * 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 { - /* tslint:disable:no-any */ - const symbol: ts.Symbol = (declaration as any).symbol; - /* tslint:enable:no-any */ - return symbol; - } - /** * Same semantics as tryGetSymbolForDeclaration(), but throws an exception if the symbol * 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'); @@ -103,48 +85,12 @@ export class TypeScriptHelpers { if (declarationWithModuleSpecifier.moduleSpecifier && ts.isStringLiteralLike(declarationWithModuleSpecifier.moduleSpecifier)) { - return TypeScriptHelpers.getTextOfIdentifierOrLiteral(declarationWithModuleSpecifier.moduleSpecifier); + return TypeScriptInternals.getTextOfIdentifierOrLiteral(declarationWithModuleSpecifier.moduleSpecifier); } return undefined; } - /** - * 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 - - // tslint:disable-next-line:no-any - 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 - - // tslint:disable-next-line:no-any - 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 - - // tslint:disable-next-line:no-any - return (ts as any).getResolvedModule(sourceFile, moduleNameText); - } - /** * Returns an ancestor of "node", such that the ancestor, any intermediary nodes, * and the starting node match a list of expected kinds. Undefined is returned 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/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 862405f07e6..7290f5c9bc7 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -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. @@ -522,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; From 2757b66526b5a9c78c2c3987e8d0f6b98f052266 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:02:13 -0800 Subject: [PATCH 20/26] Improve documentation for new code --- .../src/analyzer/AstSymbolTable.ts | 4 ++ .../src/analyzer/ExportAnalyzer.ts | 54 +++++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index c82e1a1e201..3a27f9b61d6 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -19,6 +19,9 @@ import { ExportAnalyzer } from './ExportAnalyzer'; * 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; @@ -82,6 +85,7 @@ 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 AstModule. For example, if some code references a nested class inside diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 77c06fa0db4..5d4a4527d89 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -10,6 +10,12 @@ 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; @@ -17,6 +23,16 @@ export interface IAstSymbolTable { 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; @@ -32,8 +48,7 @@ export class ExportAnalyzer { } /** - * For a given source file, this analyzes all of its exports and produces an AstModule - * object. + * 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: @@ -112,6 +127,11 @@ export class ExportAnalyzer { 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) { @@ -129,11 +149,11 @@ export class ExportAnalyzer { // 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._matchExportDeclaration(astModule, symbol, declaration); + matchedAstSymbol = this._tryMatchExportDeclaration(astModule, symbol, declaration); if (matchedAstSymbol !== undefined) { return matchedAstSymbol; } - matchedAstSymbol = this._matchImportDeclaration(astModule, symbol, declaration); + matchedAstSymbol = this._tryMatchImportDeclaration(astModule, symbol, declaration); if (matchedAstSymbol !== undefined) { return matchedAstSymbol; } @@ -156,7 +176,7 @@ export class ExportAnalyzer { return this._astSymbolTable.fetchAstSymbol(current, true, undefined); } - private _matchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + private _tryMatchExportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, declaration: ts.Declaration): AstSymbol | undefined { const exportDeclaration: ts.ExportDeclaration | undefined @@ -199,7 +219,7 @@ export class ExportAnalyzer { return undefined; } - private _matchImportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, + private _tryMatchImportDeclaration(astModule: AstModule, exportedSymbol: ts.Symbol, declaration: ts.Declaration): AstSymbol | undefined { const importDeclaration: ts.ImportDeclaration | undefined @@ -328,21 +348,29 @@ export class ExportAnalyzer { return undefined; } - private _collectExportsFromExportStar(astModule: AstModule, exportStarDeclaration: ts.Declaration): void { + /** + * 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) { - astModule.starExportedExternalModules.add(starExportedModule); + targetAstModule.starExportedExternalModules.add(starExportedModule); } else { + // Copy exportedSymbols from the other module for (const [exportName, exportedSymbol] of starExportedModule.exportedSymbols) { - if (!astModule.exportedSymbols.has(exportName)) { - astModule.exportedSymbols.set(exportName, exportedSymbol); + if (!targetAstModule.exportedSymbols.has(exportName)) { + targetAstModule.exportedSymbols.set(exportName, exportedSymbol); } } + // Copy starExportedExternalModules from the other module for (const starExportedExternalModule of starExportedModule.starExportedExternalModules) { - astModule.starExportedExternalModules.add(starExportedExternalModule); + targetAstModule.starExportedExternalModules.add(starExportedExternalModule); } } } @@ -353,6 +381,10 @@ export class ExportAnalyzer { } } + /** + * 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' From 5cbb407bb0e0083a0b4862da6b0a52256625dbea Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:07:16 -0800 Subject: [PATCH 21/26] Fix typo --- apps/api-extractor/src/analyzer/ExportAnalyzer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 5d4a4527d89..abbe7f31017 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -117,7 +117,7 @@ export class ExportAnalyzer { } - if (astModule.isExternal) { + if (!astModule.isExternal) { for (const exportedAstSymbol of astModule.exportedSymbols.values()) { this._astSymbolTable.analyze(exportedAstSymbol); } From 5bb40884d526d7ff85210b7c1d6df281c21813db Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:10:27 -0800 Subject: [PATCH 22/26] rush change --- .../pgonzal-ae-export-star_2019-01-17-00-10.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-10.json diff --git a/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-10.json b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-10.json new file mode 100644 index 00000000000..3d82ccd1bb3 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-10.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Add support for exports of the form `export * from \"____\";`", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file From 9365de2f68b0962d82ca9978e6b16cc96a31897d Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:12:21 -0800 Subject: [PATCH 23/26] rush change --- .../pgonzal-ae-export-star_2019-01-17-00-11.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-11.json diff --git a/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-11.json b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-11.json new file mode 100644 index 00000000000..21b67175088 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-11.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Improve the analyzer to allow a declaration to be exported more than once", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file From 0904681cfe0aba153bf6f9072bbec84411329aa3 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:14:09 -0800 Subject: [PATCH 24/26] Normalize newlines in .api.ts files --- apps/api-extractor/src/api/Extractor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api-extractor/src/api/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts index 2ced10321f1..3405c053427 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 From f2e0fd8e0a9b818f2c367386a24c08df1fb38861 Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:15:01 -0800 Subject: [PATCH 25/26] rush change --- .../pgonzal-ae-export-star_2019-01-17-00-14.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-14.json diff --git a/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-14.json b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-14.json new file mode 100644 index 00000000000..9ddb533f4ee --- /dev/null +++ b/common/changes/@microsoft/api-extractor/pgonzal-ae-export-star_2019-01-17-00-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Fix inconsistent newlines in .api.ts files", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "pgonzal@users.noreply.github.com" +} \ No newline at end of file From 70d4f0bffeb689811945dd80b73c877fb45a715d Mon Sep 17 00:00:00 2001 From: pgonzal Date: Wed, 16 Jan 2019 16:22:17 -0800 Subject: [PATCH 26/26] Normalize newlines in .api.ts files --- apps/api-extractor/src/api/Extractor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/api-extractor/src/api/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts index 3405c053427..20341f32aa9 100644 --- a/apps/api-extractor/src/api/Extractor.ts +++ b/apps/api-extractor/src/api/Extractor.ts @@ -459,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}`);