UNPKG

14 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.TypesUsageEvaluator = void 0;
4const ts = require("typescript");
5const typescript_1 = require("./helpers/typescript");
6class TypesUsageEvaluator {
7 constructor(files, typeChecker) {
8 this.nodesParentsMap = new Map();
9 this.usageResultCache = new Map();
10 this.typeChecker = typeChecker;
11 this.computeUsages(files);
12 }
13 isSymbolUsedBySymbol(symbol, by) {
14 return this.isSymbolUsedBySymbolImpl(this.getActualSymbol(symbol), this.getActualSymbol(by), new Set());
15 }
16 getSymbolsUsingSymbol(symbol) {
17 return this.nodesParentsMap.get(this.getActualSymbol(symbol)) || null;
18 }
19 isSymbolUsedBySymbolImpl(fromSymbol, toSymbol, visitedSymbols) {
20 if (fromSymbol === toSymbol) {
21 return this.setUsageCacheValue(fromSymbol, toSymbol, true);
22 }
23 const cacheResult = this.usageResultCache.get(fromSymbol)?.get(toSymbol);
24 if (cacheResult !== undefined) {
25 return cacheResult;
26 }
27 const reachableNodes = this.nodesParentsMap.get(fromSymbol);
28 if (reachableNodes !== undefined) {
29 for (const symbol of reachableNodes) {
30 if (visitedSymbols.has(symbol)) {
31 continue;
32 }
33 visitedSymbols.add(symbol);
34 if (this.isSymbolUsedBySymbolImpl(symbol, toSymbol, visitedSymbols)) {
35 return this.setUsageCacheValue(fromSymbol, toSymbol, true);
36 }
37 }
38 }
39 visitedSymbols.add(fromSymbol);
40 // note that we can't save negative result here because it might be not a final one
41 // because we might ended up here because of `visitedSymbols.has(symbol)` check above
42 // while actually checking that `symbol` symbol and we will store all its "children" as `false`
43 // while in reality some of them might be `true` because of cross-references or using the same children symbols
44 return false;
45 }
46 setUsageCacheValue(fromSymbol, toSymbol, value) {
47 let fromSymbolCacheMap = this.usageResultCache.get(fromSymbol);
48 if (fromSymbolCacheMap === undefined) {
49 fromSymbolCacheMap = new Map();
50 this.usageResultCache.set(fromSymbol, fromSymbolCacheMap);
51 }
52 fromSymbolCacheMap.set(toSymbol, value);
53 return value;
54 }
55 computeUsages(files) {
56 this.nodesParentsMap.clear();
57 for (const file of files) {
58 ts.forEachChild(file, this.computeUsageForNode.bind(this));
59 }
60 }
61 // eslint-disable-next-line complexity
62 computeUsageForNode(node) {
63 if ((0, typescript_1.isDeclareModule)(node) && node.body !== undefined && ts.isModuleBlock(node.body)) {
64 const moduleSymbol = this.getSymbol(node.name);
65 for (const statement of node.body.statements) {
66 this.computeUsageForNode(statement);
67 if ((0, typescript_1.isNodeNamedDeclaration)(statement)) {
68 const nodeName = (0, typescript_1.getNodeName)(statement);
69 if (nodeName !== undefined) {
70 // a node declared in `declare module` should adds "usage" to that module
71 // so we can track its usage later if needed
72 const statementSymbol = this.getSymbol(nodeName);
73 this.addUsages(statementSymbol, moduleSymbol);
74 }
75 }
76 }
77 }
78 if ((0, typescript_1.isNodeNamedDeclaration)(node)) {
79 const nodeName = (0, typescript_1.getNodeName)(node);
80 if (nodeName !== undefined) {
81 if (ts.isObjectBindingPattern(nodeName) || ts.isArrayBindingPattern(nodeName)) {
82 for (const element of nodeName.elements) {
83 this.computeUsageForNode(element);
84 }
85 }
86 else {
87 const childSymbol = this.getSymbol(nodeName);
88 if (childSymbol !== null) {
89 this.computeUsagesRecursively(node, childSymbol);
90 }
91 }
92 }
93 }
94 if (ts.isVariableStatement(node)) {
95 for (const varDeclaration of node.declarationList.declarations) {
96 this.computeUsageForNode(varDeclaration);
97 }
98 }
99 // `export * as ns from 'mod'`
100 if (ts.isExportDeclaration(node) && node.moduleSpecifier !== undefined && node.exportClause !== undefined && ts.isNamespaceExport(node.exportClause)) {
101 this.addUsagesForNamespacedModule(node.exportClause, node.moduleSpecifier);
102 }
103 // `import * as ns from 'mod'`
104 if (ts.isImportDeclaration(node) && node.moduleSpecifier !== undefined && node.importClause?.namedBindings !== undefined && ts.isNamespaceImport(node.importClause.namedBindings)) {
105 // for namespaced imports we don't want to include module's exports into usage
106 // because only exports actually "assign" all exports to a namespace node
107 // namespaced imports affect only local scope (unless it is exported, but it handled elsewhere)
108 this.addUsagesForNamespacedModule(node.importClause.namedBindings, node.moduleSpecifier, false);
109 }
110 // `export {}` or `export {} from 'mod'`
111 if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
112 for (const exportElement of node.exportClause.elements) {
113 const exportElementSymbol = (0, typescript_1.getImportExportReferencedSymbol)(exportElement, this.typeChecker);
114 // i.e. `import * as NS from './local-module'`
115 const namespaceImportForElement = (0, typescript_1.getDeclarationsForSymbol)(exportElementSymbol).find(ts.isNamespaceImport);
116 if (namespaceImportForElement !== undefined) {
117 // the namespaced import itself doesn't add a "usage", but re-export of that imported namespace does
118 // so here we're handling the case where previously imported namespace import has been re-exported from a module
119 this.addUsagesForNamespacedModule(namespaceImportForElement, namespaceImportForElement.parent.parent.moduleSpecifier);
120 }
121 // "link" referenced symbol with its import
122 const exportElementOwnSymbol = this.getNodeOwnSymbol(exportElement.name);
123 this.addUsages(exportElementSymbol, exportElementOwnSymbol);
124 this.addUsages(this.getActualSymbol(exportElementSymbol), exportElementOwnSymbol);
125 }
126 }
127 // `export =`
128 if (ts.isExportAssignment(node) && node.isExportEquals) {
129 this.addUsagesForExportAssignment(node);
130 }
131 }
132 addUsagesForExportAssignment(exportAssignment) {
133 for (const declaration of (0, typescript_1.getDeclarationsForExportedValues)(exportAssignment, this.typeChecker)) {
134 // `declare module foobar {}` or `namespace foobar {}`
135 if (ts.isModuleDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.body !== undefined && ts.isModuleBlock(declaration.body)) {
136 const moduleSymbol = this.getSymbol(declaration.name);
137 for (const statement of declaration.body.statements) {
138 if ((0, typescript_1.isNodeNamedDeclaration)(statement) && statement.name !== undefined) {
139 const statementSymbol = this.getSymbol(statement.name);
140 if (statementSymbol !== null) {
141 // this feels counter-intuitive that we assign a statement as a parent of a module
142 // but this is what happens when you have `export=` statements
143 // you can import an interface declared in `export=` exported namespace
144 // via named import statement
145 // e.g. lets say you have `namespace foo { export interface Interface {} }; export = foo;`
146 // then you can import it like `import { Interface } from 'module'`
147 // in this case only `Interface` is used, but it is part of module `foo`
148 // which means that `foo` is used via using `Interface`
149 // if you're reading this - please stop using `export=` exports asap!
150 this.addUsages(moduleSymbol, statementSymbol);
151 }
152 }
153 }
154 }
155 }
156 }
157 addUsagesForNamespacedModule(namespaceNode, moduleSpecifier, includeExports = true) {
158 // note that we shouldn't resolve the actual symbol for the namespace
159 // as in some circumstances it will be resolved to the source file
160 // i.e. namespaceSymbol would become referencedModuleSymbol so it would be no-op
161 // but we want to add this module's usage to the map
162 const namespaceSymbol = this.getNodeOwnSymbol(namespaceNode.name);
163 const referencedSourceFileSymbol = this.getSymbol(moduleSpecifier);
164 this.addUsages(referencedSourceFileSymbol, namespaceSymbol);
165 // but in case it is not resolved to the source file we need to link them
166 const resolvedNamespaceSymbol = this.getSymbol(namespaceNode.name);
167 this.addUsages(resolvedNamespaceSymbol, namespaceSymbol);
168 if (includeExports) {
169 // if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported
170 this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol);
171 }
172 }
173 addExportsToSymbol(exports, parentSymbol, visitedSymbols = new Set()) {
174 exports?.forEach((moduleExportedSymbol, name) => {
175 if (name === ts.InternalSymbolName.ExportStar) {
176 // this means that an export contains `export * from 'module'` statement
177 for (const exportStarDeclaration of (0, typescript_1.getSymbolExportStarDeclarations)(moduleExportedSymbol)) {
178 if (exportStarDeclaration.moduleSpecifier === undefined) {
179 throw new Error(`Export star declaration does not have a module specifier '${exportStarDeclaration.getText()}'`);
180 }
181 const referencedSourceFileSymbol = this.getSymbol(exportStarDeclaration.moduleSpecifier);
182 if (visitedSymbols.has(referencedSourceFileSymbol)) {
183 continue;
184 }
185 visitedSymbols.add(referencedSourceFileSymbol);
186 this.addExportsToSymbol(referencedSourceFileSymbol.exports, parentSymbol, visitedSymbols);
187 }
188 return;
189 }
190 this.addUsages(moduleExportedSymbol, parentSymbol);
191 });
192 }
193 computeUsagesRecursively(parent, parentSymbol) {
194 ts.forEachChild(parent, (child) => {
195 if (child.kind === ts.SyntaxKind.JSDoc) {
196 return;
197 }
198 this.computeUsagesRecursively(child, parentSymbol);
199 if (ts.isIdentifier(child) || child.kind === ts.SyntaxKind.DefaultKeyword) {
200 // identifiers in labelled tuples don't have symbols for their labels
201 // so let's just skip them from collecting
202 if (ts.isNamedTupleMember(child.parent) && child.parent.name === child) {
203 return;
204 }
205 // `{ propertyName: name }` - in this case we don't need to handle `propertyName` as it has no symbol
206 if (ts.isBindingElement(child.parent) && child.parent.propertyName === child) {
207 return;
208 }
209 this.addUsages(this.getSymbol(child), parentSymbol);
210 if (!ts.isQualifiedName(child.parent)) {
211 const childOwnSymbol = this.getNodeOwnSymbol(child);
212 // i.e. `import * as NS from './local-module'`
213 const namespaceImport = (0, typescript_1.getDeclarationsForSymbol)(childOwnSymbol).find(ts.isNamespaceImport);
214 if (namespaceImport !== undefined) {
215 // if a node is an identifier and not part of a qualified name
216 // and it was created as part of namespaced import
217 // then we need to assign all exports of referenced module into that namespace
218 // because they might not be added previously while processing imports/exports
219 this.addUsagesForNamespacedModule(namespaceImport, namespaceImport.parent.parent.moduleSpecifier, true);
220 }
221 }
222 }
223 });
224 }
225 addUsages(childSymbol, parentSymbol) {
226 const childSymbols = (0, typescript_1.splitTransientSymbol)(childSymbol, this.typeChecker);
227 for (const childSplitSymbol of childSymbols) {
228 let symbols = this.nodesParentsMap.get(childSplitSymbol);
229 if (symbols === undefined) {
230 symbols = new Set();
231 this.nodesParentsMap.set(childSplitSymbol, symbols);
232 }
233 // to avoid infinite recursion
234 if (childSplitSymbol !== parentSymbol) {
235 symbols.add(parentSymbol);
236 }
237 }
238 }
239 getSymbol(node) {
240 return this.getActualSymbol(this.getNodeOwnSymbol(node));
241 }
242 getNodeOwnSymbol(node) {
243 return (0, typescript_1.getNodeOwnSymbol)(node, this.typeChecker);
244 }
245 getActualSymbol(symbol) {
246 return (0, typescript_1.getActualSymbol)(symbol, this.typeChecker);
247 }
248}
249exports.TypesUsageEvaluator = TypesUsageEvaluator;