@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
256 lines • 12.3 kB
JavaScript
;
// 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 TypeScriptHelpers_1 = require("../../utils/TypeScriptHelpers");
const AstImport_1 = require("./AstImport");
/**
* 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.
*/
class SymbolAnalyzer {
/**
* This function determines which ts.Node kinds will generate an AstDeclaration.
* These correspond to the definitions that we can add AEDoc to.
*/
static isAstDeclaration(kind) {
// (alphabetical order)
switch (kind) {
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.Constructor: // Example: new(x: number);
case ts.SyntaxKind.ConstructSignature: // Example: new(x: number);
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.EnumMember:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.IndexSignature: // Example: [key: string]: string
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
// ModuleDeclaration is used for both "module" and "namespace" declarations
case ts.SyntaxKind.ModuleDeclaration:
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.PropertySignature:
case ts.SyntaxKind.TypeAliasDeclaration:
case ts.SyntaxKind.VariableDeclaration:
return true;
}
return false;
}
/**
* For the given symbol, follow imports and type alias to find the symbol that represents
* the original definition.
*/
static followAliases(symbol, typeChecker) {
let current = symbol;
// We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name
let declarationName = undefined;
while (true) {
for (const declaration of current.declarations || []) {
const declarationNameIdentifier = 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;
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 = TypeScriptHelpers_1.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 = true;
if (current.declarations) {
// Test 1: Are we inside the sinister "declare global {" construct?
let insideDeclareGlobal = false;
const highestModuleDeclaration = TypeScriptHelpers_1.TypeScriptHelpers.findHighestParent(current.declarations[0], ts.SyntaxKind.ModuleDeclaration);
if (highestModuleDeclaration) {
if (highestModuleDeclaration.name.getText().trim() === 'global') {
insideDeclareGlobal = true;
}
}
// Test 2: Otherwise, the main heuristic for ambient declarations is by looking at the
// ts.SyntaxKind.SourceFile node to see whether it has a symbol or not (i.e. whether it
// is acting as a module or not).
if (!insideDeclareGlobal) {
const sourceFileNode = TypeScriptHelpers_1.TypeScriptHelpers.findFirstParent(current.declarations[0], ts.SyntaxKind.SourceFile);
if (sourceFileNode && !!typeChecker.getSymbolAtLocation(sourceFileNode)) {
isAmbient = false;
}
}
}
return {
followedSymbol: current,
localName: declarationName || current.name,
astImport: undefined,
isAmbient: isAmbient
};
}
/**
* Helper function for _followAliases(), for handling ts.ExportDeclaration patterns
*/
static _followAliasesForExportDeclaration(declaration, symbol, typeChecker) {
const exportDeclaration = TypeScriptHelpers_1.TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ExportDeclaration);
if (exportDeclaration) {
let exportName;
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 = declaration;
exportName = (exportSpecifier.propertyName || exportSpecifier.name).getText().trim();
}
else {
throw new Error('Unimplemented export declaration kind: ' + declaration.getText());
}
if (exportDeclaration.moduleSpecifier) {
// Examples:
// " '@microsoft/sp-lodash-subset'"
// " "lodash/has""
const modulePath = SymbolAnalyzer._getPackagePathFromModuleSpecifier(exportDeclaration.moduleSpecifier);
if (modulePath) {
return {
followedSymbol: TypeScriptHelpers_1.TypeScriptHelpers.followAliases(symbol, typeChecker),
localName: exportName,
astImport: new AstImport_1.AstImport({ modulePath, exportName }),
isAmbient: false
};
}
}
}
return undefined;
}
/**
* Helper function for _followAliases(), for handling ts.ImportDeclaration patterns
*/
static _followAliasesForImportDeclaration(declaration, symbol, typeChecker) {
const importDeclaration = TypeScriptHelpers_1.TypeScriptHelpers.findFirstParent(declaration, ts.SyntaxKind.ImportDeclaration);
if (importDeclaration) {
let exportName;
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 = declaration;
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 = 'default';
}
else {
throw new Error('Unimplemented import declaration kind: ' + declaration.getText());
}
if (importDeclaration.moduleSpecifier) {
// Examples:
// " '@microsoft/sp-lodash-subset'"
// " "lodash/has""
const modulePath = SymbolAnalyzer._getPackagePathFromModuleSpecifier(importDeclaration.moduleSpecifier);
if (modulePath) {
return {
followedSymbol: TypeScriptHelpers_1.TypeScriptHelpers.followAliases(symbol, typeChecker),
localName: symbol.name,
astImport: new AstImport_1.AstImport({ modulePath, exportName }),
isAmbient: false
};
}
}
}
return undefined;
}
static _getPackagePathFromModuleSpecifier(moduleSpecifier) {
// Examples:
// " '@microsoft/sp-lodash-subset'"
// " "lodash/has""
// " './MyClass'"
const moduleSpecifierText = moduleSpecifier.getFullText();
// Remove quotes/whitespace
const path = moduleSpecifierText
.replace(/^\s*['"]/, '')
.replace(/['"]\s*$/, '');
// Does it start with something like "./" or "../"?
// If not, then assume it's an import from an external package
if (!/^\.\.?\//.test(path)) {
return path;
}
return undefined;
}
}
exports.SymbolAnalyzer = SymbolAnalyzer;
//# sourceMappingURL=SymbolAnalyzer.js.map