"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AstSymbolTable = void 0; /* eslint-disable no-bitwise */ // for ts.SymbolFlags const ts = __importStar(require("typescript")); const node_core_library_1 = require("@rushstack/node-core-library"); const AstDeclaration_1 = require("./AstDeclaration"); const TypeScriptHelpers_1 = require("./TypeScriptHelpers"); const AstSymbol_1 = require("./AstSymbol"); const PackageMetadataManager_1 = require("./PackageMetadataManager"); const ExportAnalyzer_1 = require("./ExportAnalyzer"); const AstNamespaceImport_1 = require("./AstNamespaceImport"); const TypeScriptInternals_1 = require("./TypeScriptInternals"); const SyntaxHelpers_1 = require("./SyntaxHelpers"); const SourceFileLocationFormatter_1 = require("./SourceFileLocationFormatter"); /** * AstSymbolTable is the workhorse that builds AstSymbol and AstDeclaration objects. * It maintains a cache of already constructed objects. AstSymbolTable constructs * 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). */ class AstSymbolTable { constructor(program, typeChecker, packageJsonLookup, bundledPackageNames, messageRouter) { /** * A mapping from ts.Symbol --> AstSymbol * NOTE: The AstSymbol.followedSymbol will always be a lookup key, but additional keys * are possible. * * After following type aliases, we use this map to look up the corresponding AstSymbol. */ this._astSymbolsBySymbol = new Map(); /** * A mapping from ts.Declaration --> AstDeclaration */ this._astDeclarationsByDeclaration = new Map(); // Note that this is a mapping from specific AST nodes that we analyzed, based on the underlying symbol // for that node. this._entitiesByNode = new Map(); this._program = program; this._typeChecker = typeChecker; this._messageRouter = messageRouter; this._globalVariableAnalyzer = TypeScriptInternals_1.TypeScriptInternals.getGlobalVariableAnalyzer(program); this._packageMetadataManager = new PackageMetadataManager_1.PackageMetadataManager(packageJsonLookup, messageRouter); this._exportAnalyzer = new ExportAnalyzer_1.ExportAnalyzer(this._program, this._typeChecker, bundledPackageNames, { analyze: this.analyze.bind(this), fetchAstSymbol: this._fetchAstSymbol.bind(this) }); this._alreadyWarnedGlobalNames = new Set(); } /** * Used to analyze an entry point that belongs to the working package. */ fetchAstModuleFromWorkingPackage(sourceFile) { return this._exportAnalyzer.fetchAstModuleFromSourceFile(sourceFile, undefined, false); } /** * This crawls the specified entry point and collects the full set of exported AstSymbols. */ fetchAstModuleExportInfo(astModule) { return this._exportAnalyzer.fetchAstModuleExportInfo(astModule); } /** * Attempts to retrieve an export by name from the specified `AstModule`. * Returns undefined if no match was found. */ tryGetExportOfAstModule(exportName, astModule) { return this._exportAnalyzer.tryGetExportOfAstModule(exportName, astModule); } /** * 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 * 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 * 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.) */ analyze(astEntity) { if (astEntity instanceof AstSymbol_1.AstSymbol) { return this._analyzeAstSymbol(astEntity); } if (astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) { return this._analyzeAstNamespaceImport(astEntity); } } /** * For a given astDeclaration, this efficiently finds the child corresponding to the * specified ts.Node. It is assumed that AstDeclaration.isSupportedSyntaxKind() would return true for * that node type, and that the node is an immediate child of the provided AstDeclaration. */ // NOTE: This could be a method of AstSymbol if it had a backpointer to its AstSymbolTable. getChildAstDeclarationByNode(node, parentAstDeclaration) { if (!parentAstDeclaration.astSymbol.analyzed) { throw new Error('getChildDeclarationByNode() cannot be used for an AstSymbol that was not analyzed'); } const childAstDeclaration = this._astDeclarationsByDeclaration.get(node); if (!childAstDeclaration) { throw new Error('Child declaration not found for the specified node'); } if (childAstDeclaration.parent !== parentAstDeclaration) { throw new node_core_library_1.InternalError('The found child is not attached to the parent AstDeclaration'); } return childAstDeclaration; } /** * For a given ts.Identifier that is part of an AstSymbol that we analyzed, return the AstEntity that * it refers to. Returns undefined if it doesn't refer to anything interesting. * @remarks * Throws an Error if the ts.Identifier is not part of node tree that was analyzed. */ tryGetEntityForNode(identifier) { if (!this._entitiesByNode.has(identifier)) { throw new node_core_library_1.InternalError('tryGetEntityForIdentifier() called for an identifier that was not analyzed'); } return this._entitiesByNode.get(identifier); } /** * Builds an AstSymbol.localName for a given ts.Symbol. In the current implementation, the localName is * a TypeScript-like expression that may be a string literal or ECMAScript symbol expression. * * ```ts * class X { * // localName="identifier" * public identifier: number = 1; * // localName="\"identifier\"" * public "quoted string!": number = 2; * // localName="[MyNamespace.MySymbol]" * public [MyNamespace.MySymbol]: number = 3; * } * ``` */ static getLocalNameForSymbol(symbol) { // TypeScript binds well-known ECMAScript symbols like "[Symbol.iterator]" as "__@iterator". // Decode it back into "[Symbol.iterator]". const wellKnownSymbolName = TypeScriptHelpers_1.TypeScriptHelpers.tryDecodeWellKnownSymbolName(symbol.escapedName); if (wellKnownSymbolName) { return wellKnownSymbolName; } const isUniqueSymbol = TypeScriptHelpers_1.TypeScriptHelpers.isUniqueSymbolName(symbol.escapedName); // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name. let unquotedName = symbol.name; for (const declaration of symbol.declarations || []) { // Handle cases such as "export default class X { }" where the symbol name is "default" // but the local name is "X". const localSymbol = TypeScriptInternals_1.TypeScriptInternals.tryGetLocalSymbol(declaration); if (localSymbol) { unquotedName = localSymbol.name; } // If it is a non-well-known symbol, then return the late-bound name. For example, "X.Y.z" in this example: // // namespace X { // export namespace Y { // export const z: unique symbol = Symbol("z"); // } // } // // class C { // public [X.Y.z](): void { } // } // if (isUniqueSymbol) { const declarationName = ts.getNameOfDeclaration(declaration); if (declarationName && ts.isComputedPropertyName(declarationName)) { const lateBoundName = TypeScriptHelpers_1.TypeScriptHelpers.tryGetLateBoundName(declarationName); if (lateBoundName) { // Here the string may contain an expression such as "[X.Y.z]". Names starting with "[" are always // expressions. If a string literal contains those characters, the code below will JSON.stringify() it // to avoid a collision. return lateBoundName; } } } } // Otherwise that name may come from a quoted string or pseudonym like `__constructor`. // If the string is not a safe identifier, then we must add quotes. // Note that if it was quoted but did not need to be quoted, here we will remove the quotes. if (!SyntaxHelpers_1.SyntaxHelpers.isSafeUnquotedMemberIdentifier(unquotedName)) { // For API Extractor's purposes, a canonical form is more appropriate than trying to reflect whatever // appeared in the source code. The code is not even guaranteed to be consistent, for example: // // class X { // public "f1"(x: string): void; // public f1(x: boolean): void; // public 'f1'(x: string | boolean): void { } // } return JSON.stringify(unquotedName); } return unquotedName; } _analyzeAstNamespaceImport(astNamespaceImport) { if (astNamespaceImport.analyzed) { return; } // mark before actual analyzing, to handle module cyclic reexport astNamespaceImport.analyzed = true; const exportedLocalEntities = this.fetchAstModuleExportInfo(astNamespaceImport.astModule).exportedLocalEntities; for (const exportedEntity of exportedLocalEntities.values()) { this.analyze(exportedEntity); } } _analyzeAstSymbol(astSymbol) { if (astSymbol.analyzed) { return; } if (astSymbol.nominalAnalysis) { // We don't analyze nominal symbols astSymbol._notifyAnalyzed(); return; } // Start at the root of the tree const rootAstSymbol = astSymbol.rootAstSymbol; // Calculate the full child tree for each definition for (const astDeclaration of rootAstSymbol.astDeclarations) { this._analyzeChildTree(astDeclaration.declaration, astDeclaration); } rootAstSymbol._notifyAnalyzed(); if (!astSymbol.isExternal) { // If this symbol is non-external (i.e. it belongs to the working package), then we also analyze any // referencedAstSymbols that are non-external. For example, this ensures that forgotten exports // get analyzed. rootAstSymbol.forEachDeclarationRecursive((astDeclaration) => { for (const referencedAstEntity of astDeclaration.referencedAstEntities) { // Walk up to the root of the tree, looking for any imports along the way if (referencedAstEntity instanceof AstSymbol_1.AstSymbol) { if (!referencedAstEntity.isExternal) { this._analyzeAstSymbol(referencedAstEntity); } } if (referencedAstEntity instanceof AstNamespaceImport_1.AstNamespaceImport) { if (!referencedAstEntity.astModule.isExternal) { this._analyzeAstNamespaceImport(referencedAstEntity); } } } }); } } /** * Used by analyze to recursively analyze the entire child tree. */ _analyzeChildTree(node, governingAstDeclaration) { switch (node.kind) { case ts.SyntaxKind.JSDocComment: // Skip JSDoc comments - TS considers @param tags TypeReference nodes return; // Is this a reference to another AstSymbol? case ts.SyntaxKind.TypeReference: // general type references case ts.SyntaxKind.ExpressionWithTypeArguments: // special case for e.g. the "extends" keyword case ts.SyntaxKind.ComputedPropertyName: // used for EcmaScript "symbols", e.g. "[toPrimitive]". case ts.SyntaxKind.TypeQuery: // represents for "typeof X" as a type { // Sometimes the type reference will involve multiple identifiers, e.g. "a.b.C". // In this case, we only need to worry about importing the first identifier, // so do a depth-first search for it: const identifierNode = TypeScriptHelpers_1.TypeScriptHelpers.findFirstChildNode(node, ts.SyntaxKind.Identifier); if (identifierNode) { let referencedAstEntity = this._entitiesByNode.get(identifierNode); if (!referencedAstEntity) { const symbol = this._typeChecker.getSymbolAtLocation(identifierNode); if (!symbol) { throw new Error('Symbol not found for identifier: ' + identifierNode.getText()); } // Normally we expect getSymbolAtLocation() to take us to a declaration within the same source // file, or else to an explicit "import" statement within the same source file. But in certain // situations (e.g. a global variable) the symbol will refer to a declaration in some other // source file. We'll call that case a "displaced symbol". // // For more info, see this discussion: // https://github.com/microsoft/rushstack/issues/1765#issuecomment-595559849 let displacedSymbol = true; for (const declaration of symbol.declarations || []) { if (declaration.getSourceFile() === identifierNode.getSourceFile()) { displacedSymbol = false; break; } } if (displacedSymbol) { if (this._globalVariableAnalyzer.hasGlobalName(identifierNode.text)) { // If the displaced symbol is a global variable, then API Extractor simply ignores it. // Ambient declarations typically describe the runtime environment (provided by an API consumer), // so we don't bother analyzing them as an API contract. (There are probably some packages // that include interesting global variables in their API, but API Extractor doesn't support // that yet; it would be a feature request.) if (this._messageRouter.showDiagnostics) { if (!this._alreadyWarnedGlobalNames.has(identifierNode.text)) { this._alreadyWarnedGlobalNames.add(identifierNode.text); this._messageRouter.logDiagnostic(`Ignoring reference to global variable "${identifierNode.text}"` + ` in ` + SourceFileLocationFormatter_1.SourceFileLocationFormatter.formatDeclaration(identifierNode)); } } } else { // If you encounter this, please report a bug with a repro. We're interested to know // how it can occur. throw new node_core_library_1.InternalError(`Unable to follow symbol for "${identifierNode.text}"`); } } else { referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity(symbol, governingAstDeclaration.astSymbol.isExternal); this._entitiesByNode.set(identifierNode, referencedAstEntity); } } if (referencedAstEntity) { governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity); } } } break; // Is this the identifier for the governingAstDeclaration? case ts.SyntaxKind.Identifier: { const identifierNode = node; if (!this._entitiesByNode.has(identifierNode)) { const symbol = this._typeChecker.getSymbolAtLocation(identifierNode); let referencedAstEntity = undefined; if (symbol === governingAstDeclaration.astSymbol.followedSymbol) { referencedAstEntity = this._fetchEntityForNode(identifierNode, governingAstDeclaration); } this._entitiesByNode.set(identifierNode, referencedAstEntity); } } break; case ts.SyntaxKind.ImportType: { const importTypeNode = node; let referencedAstEntity = this._entitiesByNode.get(importTypeNode); if (!this._entitiesByNode.has(importTypeNode)) { referencedAstEntity = this._fetchEntityForNode(importTypeNode, governingAstDeclaration); if (!referencedAstEntity) { // This should never happen throw new Error('Failed to fetch entity for import() type node: ' + importTypeNode.getText()); } this._entitiesByNode.set(importTypeNode, referencedAstEntity); } if (referencedAstEntity) { governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity); } } break; } // Is this node declaring a new AstSymbol? const newGoverningAstDeclaration = this._fetchAstDeclaration(node, governingAstDeclaration.astSymbol.isExternal); for (const childNode of node.getChildren()) { this._analyzeChildTree(childNode, newGoverningAstDeclaration || governingAstDeclaration); } } _fetchEntityForNode(node, governingAstDeclaration) { let referencedAstEntity = this._entitiesByNode.get(node); if (!referencedAstEntity) { if (node.kind === ts.SyntaxKind.ImportType) { referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntityFromImportTypeNode(node, governingAstDeclaration.astSymbol.isExternal); } else { const symbol = this._typeChecker.getSymbolAtLocation(node); if (!symbol) { throw new Error('Symbol not found for identifier: ' + node.getText()); } referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity(symbol, governingAstDeclaration.astSymbol.isExternal); } this._entitiesByNode.set(node, referencedAstEntity); } return referencedAstEntity; } _fetchAstDeclaration(node, isExternal) { if (!AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(node.kind)) { return undefined; } const symbol = TypeScriptHelpers_1.TypeScriptHelpers.getSymbolForDeclaration(node, this._typeChecker); if (!symbol) { throw new node_core_library_1.InternalError('Unable to find symbol for node'); } const astSymbol = this._fetchAstSymbol({ followedSymbol: symbol, isExternal: isExternal, includeNominalAnalysis: true, addIfMissing: true }); if (!astSymbol) { return undefined; } const astDeclaration = this._astDeclarationsByDeclaration.get(node); if (!astDeclaration) { throw new node_core_library_1.InternalError('Unable to find constructed AstDeclaration'); } return astDeclaration; } _fetchAstSymbol(options) { const followedSymbol = options.followedSymbol; // Filter out symbols representing constructs that we don't care about const arbitraryDeclaration = TypeScriptHelpers_1.TypeScriptHelpers.tryGetADeclaration(followedSymbol); if (!arbitraryDeclaration) { return undefined; } if (followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient)) { if (!TypeScriptInternals_1.TypeScriptInternals.isLateBoundSymbol(followedSymbol)) { return undefined; } } // API Extractor doesn't analyze ambient declarations at all if (TypeScriptHelpers_1.TypeScriptHelpers.isAmbient(followedSymbol, this._typeChecker)) { // We make a special exemption for ambient declarations that appear in a source file containing // an "export=" declaration that allows them to be imported as non-ambient. if (!this._exportAnalyzer.isImportableAmbientSourceFile(arbitraryDeclaration.getSourceFile())) { return undefined; } } // Make sure followedSymbol isn't an alias for something else if (TypeScriptHelpers_1.TypeScriptHelpers.isFollowableAlias(followedSymbol, this._typeChecker)) { // We expect the caller to have already followed any aliases throw new node_core_library_1.InternalError('AstSymbolTable._fetchAstSymbol() cannot be called with a symbol alias'); } let astSymbol = this._astSymbolsBySymbol.get(followedSymbol); if (!astSymbol) { // None of the above lookups worked, so create a new entry... let nominalAnalysis = false; if (options.isExternal) { // If the file is from an external package that does not support AEDoc, normally we ignore it completely. // But in some cases (e.g. checking star exports of an external package) we need an AstSymbol to // represent it, but we don't need to analyze its sibling/children. const followedSymbolSourceFileName = arbitraryDeclaration.getSourceFile().fileName; if (!this._packageMetadataManager.isAedocSupportedFor(followedSymbolSourceFileName)) { nominalAnalysis = true; if (!options.includeNominalAnalysis) { return undefined; } } } let parentAstSymbol = undefined; if (!nominalAnalysis) { for (const declaration of followedSymbol.declarations || []) { if (!AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(declaration.kind)) { throw new node_core_library_1.InternalError(`The "${followedSymbol.name}" symbol has a` + ` ts.SyntaxKind.${ts.SyntaxKind[declaration.kind]} declaration which is not (yet?)` + ` supported by API Extractor`); } } // We always fetch the entire chain of parents for each declaration. // (Children/siblings are only analyzed on demand.) // Key assumptions behind this squirrely logic: // // IF a given symbol has two declarations D1 and D2; AND // If D1 has a parent P1, then // - D2 will also have a parent P2; AND // - P1 and P2's symbol will be the same // - but P1 and P2 may be different (e.g. merged namespaces containing merged interfaces) // Is there a parent AstSymbol? First we check to see if there is a parent declaration: if (arbitraryDeclaration) { const arbitraryParentDeclaration = this._tryFindFirstAstDeclarationParent(arbitraryDeclaration); if (arbitraryParentDeclaration) { const parentSymbol = TypeScriptHelpers_1.TypeScriptHelpers.getSymbolForDeclaration(arbitraryParentDeclaration, this._typeChecker); parentAstSymbol = this._fetchAstSymbol({ followedSymbol: parentSymbol, isExternal: options.isExternal, includeNominalAnalysis: false, addIfMissing: true }); if (!parentAstSymbol) { throw new node_core_library_1.InternalError('Unable to construct a parent AstSymbol for ' + followedSymbol.name); } } } } const localName = options.localName || AstSymbolTable.getLocalNameForSymbol(followedSymbol); astSymbol = new AstSymbol_1.AstSymbol({ followedSymbol: followedSymbol, localName: localName, isExternal: options.isExternal, nominalAnalysis: nominalAnalysis, parentAstSymbol: parentAstSymbol, rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined }); this._astSymbolsBySymbol.set(followedSymbol, astSymbol); // Okay, now while creating the declarations we will wire them up to the // their corresponding parent declarations for (const declaration of followedSymbol.declarations || []) { let parentAstDeclaration = undefined; if (parentAstSymbol) { const parentDeclaration = this._tryFindFirstAstDeclarationParent(declaration); if (!parentDeclaration) { throw new node_core_library_1.InternalError('Missing parent declaration'); } parentAstDeclaration = this._astDeclarationsByDeclaration.get(parentDeclaration); if (!parentAstDeclaration) { throw new node_core_library_1.InternalError('Missing parent AstDeclaration'); } } const astDeclaration = new AstDeclaration_1.AstDeclaration({ declaration, astSymbol, parent: parentAstDeclaration }); this._astDeclarationsByDeclaration.set(declaration, astDeclaration); } } if (options.isExternal !== astSymbol.isExternal) { throw new node_core_library_1.InternalError(`Cannot assign isExternal=${options.isExternal} for` + ` the symbol ${astSymbol.localName} because it was previously registered` + ` with isExternal=${astSymbol.isExternal}`); } return astSymbol; } /** * Returns the first parent satisfying isAstDeclaration(), or undefined if none is found. */ _tryFindFirstAstDeclarationParent(node) { let currentNode = node.parent; while (currentNode) { if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(currentNode.kind)) { return currentNode; } currentNode = currentNode.parent; } return undefined; } } exports.AstSymbolTable = AstSymbolTable; //# sourceMappingURL=AstSymbolTable.js.map