UNPKG

dts-bundle-generator

Version:
250 lines (249 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypesUsageEvaluator = void 0; const ts = require("typescript"); const typescript_1 = require("./helpers/typescript"); class TypesUsageEvaluator { constructor(files, typeChecker) { this.nodesParentsMap = new Map(); this.usageResultCache = new Map(); this.typeChecker = typeChecker; this.computeUsages(files); } isSymbolUsedBySymbol(symbol, by) { return this.isSymbolUsedBySymbolImpl(this.getActualSymbol(symbol), this.getActualSymbol(by), new Set()); } getSymbolsUsingSymbol(symbol) { return this.nodesParentsMap.get(this.getActualSymbol(symbol)) || null; } isSymbolUsedBySymbolImpl(fromSymbol, toSymbol, visitedSymbols) { if (fromSymbol === toSymbol) { return this.setUsageCacheValue(fromSymbol, toSymbol, true); } const cacheResult = this.usageResultCache.get(fromSymbol)?.get(toSymbol); if (cacheResult !== undefined) { return cacheResult; } const reachableNodes = this.nodesParentsMap.get(fromSymbol); if (reachableNodes !== undefined) { for (const symbol of reachableNodes) { if (visitedSymbols.has(symbol)) { continue; } visitedSymbols.add(symbol); if (this.isSymbolUsedBySymbolImpl(symbol, toSymbol, visitedSymbols)) { return this.setUsageCacheValue(fromSymbol, toSymbol, true); } } } visitedSymbols.add(fromSymbol); // note that we can't save negative result here because it might be not a final one // because we might ended up here because of `visitedSymbols.has(symbol)` check above // while actually checking that `symbol` symbol and we will store all its "children" as `false` // while in reality some of them might be `true` because of cross-references or using the same children symbols return false; } setUsageCacheValue(fromSymbol, toSymbol, value) { let fromSymbolCacheMap = this.usageResultCache.get(fromSymbol); if (fromSymbolCacheMap === undefined) { fromSymbolCacheMap = new Map(); this.usageResultCache.set(fromSymbol, fromSymbolCacheMap); } fromSymbolCacheMap.set(toSymbol, value); return value; } computeUsages(files) { this.nodesParentsMap.clear(); for (const file of files) { ts.forEachChild(file, this.computeUsageForNode.bind(this)); } } // eslint-disable-next-line complexity computeUsageForNode(node) { if ((0, typescript_1.isDeclareModule)(node) && node.body !== undefined && ts.isModuleBlock(node.body)) { const moduleSymbol = this.getSymbol(node.name); for (const statement of node.body.statements) { this.computeUsageForNode(statement); if ((0, typescript_1.isNodeNamedDeclaration)(statement)) { const nodeName = (0, typescript_1.getNodeName)(statement); if (nodeName !== undefined) { // a node declared in `declare module` should adds "usage" to that module // so we can track its usage later if needed const statementSymbol = this.getSymbol(nodeName); this.addUsages(statementSymbol, moduleSymbol); } } } } if ((0, typescript_1.isNodeNamedDeclaration)(node)) { const nodeName = (0, typescript_1.getNodeName)(node); if (nodeName !== undefined) { if (ts.isObjectBindingPattern(nodeName) || ts.isArrayBindingPattern(nodeName)) { for (const element of nodeName.elements) { this.computeUsageForNode(element); } } else { const childSymbol = this.getSymbol(nodeName); if (childSymbol !== null) { this.computeUsagesRecursively(node, childSymbol); } } } } if (ts.isVariableStatement(node)) { for (const varDeclaration of node.declarationList.declarations) { this.computeUsageForNode(varDeclaration); } } // `export * as ns from 'mod'` if (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined && node.exportClause !== undefined && ts.isNamespaceExport(node.exportClause)) { this.addUsagesForNamespacedModule(node.exportClause, node.moduleSpecifier); } // `import * as ns from 'mod'` if (ts.isImportDeclaration(node) && node.moduleSpecifier !== undefined && node.importClause?.namedBindings !== undefined && ts.isNamespaceImport(node.importClause.namedBindings)) { // for namespaced imports we don't want to include module's exports into usage // because only exports actually "assign" all exports to a namespace node // namespaced imports affect only local scope (unless it is exported, but it handled elsewhere) this.addUsagesForNamespacedModule(node.importClause.namedBindings, node.moduleSpecifier, false); } // `export {}` or `export {} from 'mod'` if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) { for (const exportElement of node.exportClause.elements) { const exportElementSymbol = (0, typescript_1.getImportExportReferencedSymbol)(exportElement, this.typeChecker); // i.e. `import * as NS from './local-module'` const namespaceImportForElement = (0, typescript_1.getDeclarationsForSymbol)(exportElementSymbol).find(ts.isNamespaceImport); if (namespaceImportForElement !== undefined) { // the namespaced import itself doesn't add a "usage", but re-export of that imported namespace does // so here we're handling the case where previously imported namespace import has been re-exported from a module this.addUsagesForNamespacedModule(namespaceImportForElement, namespaceImportForElement.parent.parent.moduleSpecifier); } // "link" referenced symbol with its import const exportElementOwnSymbol = this.getNodeOwnSymbol(exportElement.name); this.addUsages(exportElementSymbol, exportElementOwnSymbol); this.addUsages(this.getActualSymbol(exportElementSymbol), exportElementOwnSymbol); } } // `export =` if (ts.isExportAssignment(node) && node.isExportEquals) { this.addUsagesForExportAssignment(node); } } addUsagesForExportAssignment(exportAssignment) { for (const declaration of (0, typescript_1.getDeclarationsForExportedValues)(exportAssignment, this.typeChecker)) { // `declare module foobar {}` or `namespace foobar {}` if (ts.isModuleDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.body !== undefined && ts.isModuleBlock(declaration.body)) { const moduleSymbol = this.getSymbol(declaration.name); for (const statement of declaration.body.statements) { if ((0, typescript_1.isNodeNamedDeclaration)(statement) && statement.name !== undefined) { const statementSymbol = this.getSymbol(statement.name); if (statementSymbol !== null) { // this feels counter-intuitive that we assign a statement as a parent of a module // but this is what happens when you have `export=` statements // you can import an interface declared in `export=` exported namespace // via named import statement // e.g. lets say you have `namespace foo { export interface Interface {} }; export = foo;` // then you can import it like `import { Interface } from 'module'` // in this case only `Interface` is used, but it is part of module `foo` // which means that `foo` is used via using `Interface` // if you're reading this - please stop using `export=` exports asap! this.addUsages(moduleSymbol, statementSymbol); } } } } } } addUsagesForNamespacedModule(namespaceNode, moduleSpecifier, includeExports = true) { // note that we shouldn't resolve the actual symbol for the namespace // as in some circumstances it will be resolved to the source file // i.e. namespaceSymbol would become referencedModuleSymbol so it would be no-op // but we want to add this module's usage to the map const namespaceSymbol = this.getNodeOwnSymbol(namespaceNode.name); const referencedSourceFileSymbol = this.getSymbol(moduleSpecifier); this.addUsages(referencedSourceFileSymbol, namespaceSymbol); // but in case it is not resolved to the source file we need to link them const resolvedNamespaceSymbol = this.getSymbol(namespaceNode.name); this.addUsages(resolvedNamespaceSymbol, namespaceSymbol); if (includeExports) { // if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol); } } addExportsToSymbol(exports, parentSymbol, visitedSymbols = new Set()) { exports?.forEach((moduleExportedSymbol, name) => { if (name === ts.InternalSymbolName.ExportStar) { // this means that an export contains `export * from 'module'` statement for (const exportStarDeclaration of (0, typescript_1.getSymbolExportStarDeclarations)(moduleExportedSymbol)) { if (exportStarDeclaration.moduleSpecifier === undefined) { throw new Error(`Export star declaration does not have a module specifier '${exportStarDeclaration.getText()}'`); } const referencedSourceFileSymbol = this.getSymbol(exportStarDeclaration.moduleSpecifier); if (visitedSymbols.has(referencedSourceFileSymbol)) { continue; } visitedSymbols.add(referencedSourceFileSymbol); this.addExportsToSymbol(referencedSourceFileSymbol.exports, parentSymbol, visitedSymbols); } return; } this.addUsages(moduleExportedSymbol, parentSymbol); }); } computeUsagesRecursively(parent, parentSymbol) { ts.forEachChild(parent, (child) => { if (child.kind === ts.SyntaxKind.JSDoc) { return; } this.computeUsagesRecursively(child, parentSymbol); if (ts.isIdentifier(child) || child.kind === ts.SyntaxKind.DefaultKeyword) { // identifiers in labelled tuples don't have symbols for their labels // so let's just skip them from collecting if (ts.isNamedTupleMember(child.parent) && child.parent.name === child) { return; } // `{ propertyName: name }` - in this case we don't need to handle `propertyName` as it has no symbol if (ts.isBindingElement(child.parent) && child.parent.propertyName === child) { return; } this.addUsages(this.getSymbol(child), parentSymbol); if (!ts.isQualifiedName(child.parent)) { const childOwnSymbol = this.getNodeOwnSymbol(child); // i.e. `import * as NS from './local-module'` const namespaceImport = (0, typescript_1.getDeclarationsForSymbol)(childOwnSymbol).find(ts.isNamespaceImport); if (namespaceImport !== undefined) { // if a node is an identifier and not part of a qualified name // and it was created as part of namespaced import // then we need to assign all exports of referenced module into that namespace // because they might not be added previously while processing imports/exports this.addUsagesForNamespacedModule(namespaceImport, namespaceImport.parent.parent.moduleSpecifier, true); } } } }); } addUsages(childSymbol, parentSymbol) { const childSymbols = (0, typescript_1.splitTransientSymbol)(childSymbol, this.typeChecker); for (const childSplitSymbol of childSymbols) { let symbols = this.nodesParentsMap.get(childSplitSymbol); if (symbols === undefined) { symbols = new Set(); this.nodesParentsMap.set(childSplitSymbol, symbols); } // to avoid infinite recursion if (childSplitSymbol !== parentSymbol) { symbols.add(parentSymbol); } } } getSymbol(node) { return this.getActualSymbol(this.getNodeOwnSymbol(node)); } getNodeOwnSymbol(node) { return (0, typescript_1.getNodeOwnSymbol)(node, this.typeChecker); } getActualSymbol(symbol) { return (0, typescript_1.getActualSymbol)(symbol, this.typeChecker); } } exports.TypesUsageEvaluator = TypesUsageEvaluator;