@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
349 lines • 18.8 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
/* tslint:disable:no-bitwise */
const ts = require("typescript");
const AstDeclaration_1 = require("./AstDeclaration");
const SymbolAnalyzer_1 = require("./SymbolAnalyzer");
const TypeScriptHelpers_1 = require("../../utils/TypeScriptHelpers");
const AstSymbol_1 = require("./AstSymbol");
const AstEntryPoint_1 = require("./AstEntryPoint");
const PackageMetadataManager_1 = require("./PackageMetadataManager");
/**
* 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
* any particular entry point. (For example, it does not track whether a given AstSymbol
* is "exported" or not.)
*/
class AstSymbolTable {
constructor(typeChecker, packageJsonLookup) {
/**
* 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();
/**
* A mapping from AstImport.key --> AstSymbol.
*
* If AstSymbol.astImport is undefined, then it is not included in the map.
*/
this._astSymbolsByImportKey = new Map();
/**
* Cache of fetchEntryPoint() results.
*/
this._astEntryPointsBySourceFile = new Map();
this._typeChecker = typeChecker;
this._packageJsonLookup = packageJsonLookup;
this._packageMetadataManager = new PackageMetadataManager_1.PackageMetadataManager(packageJsonLookup);
}
/**
* For a given source file, this analyzes all of its exports and produces an AstEntryPoint
* object.
*/
fetchEntryPoint(sourceFile) {
let astEntryPoint = this._astEntryPointsBySourceFile.get(sourceFile);
if (!astEntryPoint) {
const rootFileSymbol = TypeScriptHelpers_1.TypeScriptHelpers.getSymbolForDeclaration(sourceFile);
if (!rootFileSymbol.declarations || !rootFileSymbol.declarations.length) {
throw new Error('Unable to find a root declaration for ' + sourceFile.fileName);
}
if (!this._packageMetadataManager.isAedocSupportedFor(sourceFile.fileName)) {
const packageJsonPath = this._packageJsonLookup
.tryGetPackageJsonFilePathFor(sourceFile.fileName);
if (packageJsonPath) {
throw new Error(`Please add a field such as "tsdoc": { "tsdocFlavor": "AEDoc" } to this file:\n`
+ packageJsonPath);
}
else {
throw new Error(`The specified entry point does not appear to have an associated package.json file:\n`
+ sourceFile.fileName);
}
}
const exportSymbols = this._typeChecker.getExportsOfModule(rootFileSymbol) || [];
const exportedMembers = [];
for (const exportSymbol of exportSymbols) {
const astSymbol = this._fetchAstSymbol(exportSymbol, true);
if (!astSymbol) {
throw new Error('Unsupported export: ' + exportSymbol.name);
}
this.analyze(astSymbol);
exportedMembers.push({ name: exportSymbol.name, astSymbol: astSymbol });
}
astEntryPoint = new AstEntryPoint_1.AstEntryPoint({ exportedMembers });
this._astEntryPointsBySourceFile.set(sourceFile, astEntryPoint);
}
return astEntryPoint;
}
/**
* 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 AstEntryPoint. 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(astSymbol) {
if (astSymbol.analyzed) {
return;
}
if (astSymbol.nominal) {
// 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.astImport) {
// If this symbol is not imported, then we also analyze any referencedAstSymbols
// that are not imported. For example, this ensures that forgotten exports get
// analyzed.
rootAstSymbol.forEachDeclarationRecursive((astDeclaration) => {
for (const referencedAstSymbol of astDeclaration.referencedAstSymbols) {
// Walk up to the root of the tree, looking for any imports along the way
if (!referencedAstSymbol.imported) {
this.analyze(referencedAstSymbol);
}
}
});
}
}
/**
* Looks up the AstSymbol corresponding to the given ts.Symbol.
* This will not analyze or construct any new AstSymbol objects.
*/
tryGetAstSymbol(symbol) {
return this._fetchAstSymbol(symbol, false);
}
/**
* For a given astDeclaration, this efficiently finds the child corresponding to the
* specified ts.Node. It is assumed that isAstDeclaration() 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 Error('Program Bug: The found child is not attached to the parent AstDeclaration');
}
return childAstDeclaration;
}
/**
* 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
{
// 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 symbolNode = TypeScriptHelpers_1.TypeScriptHelpers.findFirstChildNode(node, ts.SyntaxKind.Identifier);
if (!symbolNode) {
break;
}
const symbol = this._typeChecker.getSymbolAtLocation(symbolNode);
if (!symbol) {
throw new Error('Symbol not found for identifier: ' + symbolNode.getText());
}
const referencedAstSymbol = this._fetchAstSymbol(symbol, true);
if (referencedAstSymbol) {
governingAstDeclaration._notifyReferencedAstSymbol(referencedAstSymbol);
}
}
break;
}
// Is this node declaring a new AstSymbol?
const newGoverningAstDeclaration = this._fetchAstDeclaration(node);
for (const childNode of node.getChildren()) {
this._analyzeChildTree(childNode, newGoverningAstDeclaration || governingAstDeclaration);
}
}
// tslint:disable-next-line:no-unused-variable
_fetchAstDeclaration(node) {
const astSymbol = this._fetchAstSymbolForNode(node);
if (!astSymbol) {
return undefined;
}
const astDeclaration = this._astDeclarationsByDeclaration.get(node);
if (!astDeclaration) {
throw new Error('Program Bug: Unable to find constructed AstDeclaration');
}
return astDeclaration;
}
_fetchAstSymbolForNode(node) {
if (!SymbolAnalyzer_1.SymbolAnalyzer.isAstDeclaration(node.kind)) {
return undefined;
}
const symbol = TypeScriptHelpers_1.TypeScriptHelpers.getSymbolForDeclaration(node);
if (!symbol) {
throw new Error('Program Bug: Unable to find symbol for node');
}
return this._fetchAstSymbol(symbol, true);
}
_fetchAstSymbol(symbol, addIfMissing) {
const followAliasesResult = SymbolAnalyzer_1.SymbolAnalyzer.followAliases(symbol, this._typeChecker);
const followedSymbol = followAliasesResult.followedSymbol;
// 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) {
return undefined;
}
let astSymbol = this._astSymbolsBySymbol.get(followedSymbol);
if (!astSymbol) {
if (!followedSymbol.declarations || followedSymbol.declarations.length < 1) {
throw new Error('Program Bug: Followed a symbol with no declarations');
}
const astImport = followAliasesResult.astImport;
if (astImport) {
if (!astSymbol) {
astSymbol = this._astSymbolsByImportKey.get(astImport.key);
if (astSymbol) {
// We didn't find the entry using followedSymbol, but we did using importPackageKey,
// so add a mapping for followedSymbol; we'll need it later when renaming identifiers
this._astSymbolsBySymbol.set(followedSymbol, astSymbol);
}
}
}
if (!astSymbol) {
// None of the above lookups worked, so create a new entry...
let nominal = 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:
//
// import * as semver1 from 'semver';
//
// To handle the expression "semver1.SemVer", we need "semver1" to map to an AstSymbol
// that causes us to emit the above import. However we do NOT want it to act as the root
// of a declaration tree, because in general the *.d.ts generator is trying to roll up
// definitions and eliminate source files. So, even though isAstDeclaration() would return
// 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;
}
// 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.
if (!this._packageMetadataManager.isAedocSupportedFor(followedSymbol.declarations[0].getSourceFile().fileName)) {
nominal = true;
}
let parentAstSymbol = undefined;
if (!nominal) {
for (const declaration of followedSymbol.declarations || []) {
if (!SymbolAnalyzer_1.SymbolAnalyzer.isAstDeclaration(declaration.kind)) {
throw new Error(`Program Bug: The "${followedSymbol.name}" symbol uses the construct`
+ ` "${ts.SyntaxKind[declaration.kind]}" which may be an unimplemented language feature`);
}
}
// 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:
const arbitaryParentDeclaration = this._tryFindFirstAstDeclarationParent(followedSymbol.declarations[0]);
if (arbitaryParentDeclaration) {
const parentSymbol = TypeScriptHelpers_1.TypeScriptHelpers.getSymbolForDeclaration(arbitaryParentDeclaration);
parentAstSymbol = this._fetchAstSymbol(parentSymbol, addIfMissing);
if (!parentAstSymbol) {
throw new Error('Program bug: Unable to construct a parent AstSymbol for '
+ followedSymbol.name);
}
}
}
astSymbol = new AstSymbol_1.AstSymbol({
localName: followAliasesResult.localName,
followedSymbol: followAliasesResult.followedSymbol,
astImport: astImport,
parentAstSymbol: parentAstSymbol,
rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined,
nominal: nominal
});
this._astSymbolsBySymbol.set(followedSymbol, astSymbol);
if (astImport) {
// If it's an import, add it to the lookup
this._astSymbolsByImportKey.set(astImport.key, 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 Error('Program bug: Missing parent declaration');
}
parentAstDeclaration = this._astDeclarationsByDeclaration.get(parentDeclaration);
if (!parentAstDeclaration) {
throw new Error('Program bug: Missing parent AstDeclaration');
}
}
const astDeclaration = new AstDeclaration_1.AstDeclaration({
declaration, astSymbol, parent: parentAstDeclaration
});
this._astDeclarationsByDeclaration.set(declaration, astDeclaration);
}
}
}
if (followAliasesResult.astImport && !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
// involve an import statement.
//
// This assumption might be violated if the caller did something unusual like feeding random
// symbols to AstSymbolTable.analyze() in the middle of the analysis.
throw new Error('Program Bug: The symbol ' + astSymbol.localName + ' is being imported'
+ ' after it was already registered as non-imported');
}
return astSymbol;
}
/**
* Returns the first parent satisfying isAstDeclaration(), or undefined if none is found.
*/
_tryFindFirstAstDeclarationParent(node) {
let currentNode = node.parent;
while (currentNode) {
if (SymbolAnalyzer_1.SymbolAnalyzer.isAstDeclaration(currentNode.kind)) {
return currentNode;
}
currentNode = currentNode.parent;
}
return undefined;
}
}
exports.AstSymbolTable = AstSymbolTable;
//# sourceMappingURL=AstSymbolTable.js.map