circle-ir 4.17.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/advisories.d.ts +186 -0
- package/dist/analysis/advisories.d.ts.map +1 -0
- package/dist/analysis/advisories.js +391 -0
- package/dist/analysis/advisories.js.map +1 -0
- package/dist/analysis/comment-lines.d.ts +14 -0
- package/dist/analysis/comment-lines.d.ts.map +1 -0
- package/dist/analysis/comment-lines.js +34 -0
- package/dist/analysis/comment-lines.js.map +1 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +2 -2
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
- package/dist/analysis/constant-caller-gate.js +205 -29
- package/dist/analysis/constant-caller-gate.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +99 -0
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +72 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +75 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/maven-declared.d.ts +72 -0
- package/dist/analysis/maven-declared.d.ts.map +1 -0
- package/dist/analysis/maven-declared.js +216 -0
- package/dist/analysis/maven-declared.js.map +1 -0
- package/dist/analysis/maven-version.d.ts +28 -0
- package/dist/analysis/maven-version.d.ts.map +1 -0
- package/dist/analysis/maven-version.js +172 -0
- package/dist/analysis/maven-version.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/constant-propagation-pass.js +29 -1
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +81 -1
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +562 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1057 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +690 -12
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +133 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +29 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
- package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
- package/dist/analysis/promote-cross-file-flows.js +32 -0
- package/dist/analysis/promote-cross-file-flows.js.map +1 -0
- package/dist/analysis/sbom.d.ts +100 -0
- package/dist/analysis/sbom.d.ts.map +1 -1
- package/dist/analysis/sbom.js +514 -3
- package/dist/analysis/sbom.js.map +1 -1
- package/dist/analysis/semver-version.d.ts +30 -0
- package/dist/analysis/semver-version.d.ts.map +1 -0
- package/dist/analysis/semver-version.js +76 -0
- package/dist/analysis/semver-version.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +5 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +225 -4
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +57 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +69 -6
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +2936 -291
- package/dist/core/circle-ir-core.cjs +634 -203
- package/dist/core/circle-ir-core.js +634 -203
- package/dist/core/extractors/dfg.js +205 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
- package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
- package/dist/core/extractors/ts-declaration-detail.js +266 -0
- package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
- package/dist/graph/analysis-pass.d.ts +4 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +4 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph-facts/dead-code.d.ts +62 -0
- package/dist/graph-facts/dead-code.d.ts.map +1 -0
- package/dist/graph-facts/dead-code.js +90 -0
- package/dist/graph-facts/dead-code.js.map +1 -0
- package/dist/graph-facts/dependency-calls.d.ts +86 -0
- package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
- package/dist/graph-facts/dependency-calls.js +93 -0
- package/dist/graph-facts/dependency-calls.js.map +1 -0
- package/dist/graph-facts/index.d.ts +10 -0
- package/dist/graph-facts/index.d.ts.map +1 -0
- package/dist/graph-facts/index.js +10 -0
- package/dist/graph-facts/index.js.map +1 -0
- package/dist/graph-facts/maven-packages.d.ts +42 -0
- package/dist/graph-facts/maven-packages.d.ts.map +1 -0
- package/dist/graph-facts/maven-packages.js +133 -0
- package/dist/graph-facts/maven-packages.js.map +1 -0
- package/dist/graph-facts/reach.d.ts +203 -0
- package/dist/graph-facts/reach.d.ts.map +1 -0
- package/dist/graph-facts/reach.js +219 -0
- package/dist/graph-facts/reach.js.map +1 -0
- package/dist/graph-facts/removal-reach.d.ts +73 -0
- package/dist/graph-facts/removal-reach.d.ts.map +1 -0
- package/dist/graph-facts/removal-reach.js +64 -0
- package/dist/graph-facts/removal-reach.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/go-graph.d.ts +68 -0
- package/dist/navigation/go-graph.d.ts.map +1 -0
- package/dist/navigation/go-graph.js +753 -0
- package/dist/navigation/go-graph.js.map +1 -0
- package/dist/navigation/index.d.ts +7 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/java-entry-tables.d.ts +43 -0
- package/dist/navigation/java-entry-tables.d.ts.map +1 -0
- package/dist/navigation/java-entry-tables.js +56 -0
- package/dist/navigation/java-entry-tables.js.map +1 -0
- package/dist/navigation/java-graph.d.ts +64 -0
- package/dist/navigation/java-graph.d.ts.map +1 -0
- package/dist/navigation/java-graph.js +466 -0
- package/dist/navigation/java-graph.js.map +1 -0
- package/dist/navigation/navigation-index.d.ts +51 -1
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +72 -0
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/python-graph.d.ts +73 -0
- package/dist/navigation/python-graph.d.ts.map +1 -0
- package/dist/navigation/python-graph.js +867 -0
- package/dist/navigation/python-graph.js.map +1 -0
- package/dist/navigation/ts-call-edges.d.ts +193 -0
- package/dist/navigation/ts-call-edges.d.ts.map +1 -0
- package/dist/navigation/ts-call-edges.js +2089 -0
- package/dist/navigation/ts-call-edges.js.map +1 -0
- package/dist/navigation/ts-entry-tables.d.ts +88 -0
- package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
- package/dist/navigation/ts-entry-tables.js +50 -0
- package/dist/navigation/ts-entry-tables.js.map +1 -0
- package/dist/path-set/branches.d.ts +52 -0
- package/dist/path-set/branches.d.ts.map +1 -0
- package/dist/path-set/branches.js +124 -0
- package/dist/path-set/branches.js.map +1 -0
- package/dist/path-set/finding-diff.d.ts +63 -0
- package/dist/path-set/finding-diff.d.ts.map +1 -0
- package/dist/path-set/finding-diff.js +150 -0
- package/dist/path-set/finding-diff.js.map +1 -0
- package/dist/path-set/index.d.ts +18 -0
- package/dist/path-set/index.d.ts.map +1 -0
- package/dist/path-set/index.js +12 -0
- package/dist/path-set/index.js.map +1 -0
- package/dist/path-set/line-diff.d.ts +19 -0
- package/dist/path-set/line-diff.d.ts.map +1 -0
- package/dist/path-set/line-diff.js +98 -0
- package/dist/path-set/line-diff.js.map +1 -0
- package/dist/path-set/path-set.d.ts +88 -0
- package/dist/path-set/path-set.d.ts.map +1 -0
- package/dist/path-set/path-set.js +716 -0
- package/dist/path-set/path-set.js.map +1 -0
- package/dist/path-set/templates.d.ts +42 -0
- package/dist/path-set/templates.d.ts.map +1 -0
- package/dist/path-set/templates.js +119 -0
- package/dist/path-set/templates.js.map +1 -0
- package/dist/path-set/types.d.ts +195 -0
- package/dist/path-set/types.d.ts.map +1 -0
- package/dist/path-set/types.js +8 -0
- package/dist/path-set/types.js.map +1 -0
- package/dist/remedy-class/index.d.ts +12 -0
- package/dist/remedy-class/index.d.ts.map +1 -0
- package/dist/remedy-class/index.js +9 -0
- package/dist/remedy-class/index.js.map +1 -0
- package/dist/remedy-class/match.d.ts +165 -0
- package/dist/remedy-class/match.d.ts.map +1 -0
- package/dist/remedy-class/match.js +705 -0
- package/dist/remedy-class/match.js.map +1 -0
- package/dist/remedy-class/output-context.d.ts +25 -0
- package/dist/remedy-class/output-context.d.ts.map +1 -0
- package/dist/remedy-class/output-context.js +150 -0
- package/dist/remedy-class/output-context.js.map +1 -0
- package/dist/remedy-class/sha256.d.ts +18 -0
- package/dist/remedy-class/sha256.d.ts.map +1 -0
- package/dist/remedy-class/sha256.js +89 -0
- package/dist/remedy-class/sha256.js.map +1 -0
- package/dist/remedy-class/table.d.ts +130 -0
- package/dist/remedy-class/table.d.ts.map +1 -0
- package/dist/remedy-class/table.js +457 -0
- package/dist/remedy-class/table.js.map +1 -0
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +60 -5
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/stable-ids/builder.d.ts +20 -0
- package/dist/stable-ids/builder.d.ts.map +1 -0
- package/dist/stable-ids/builder.js +65 -0
- package/dist/stable-ids/builder.js.map +1 -0
- package/dist/stable-ids/diff.d.ts +18 -0
- package/dist/stable-ids/diff.d.ts.map +1 -0
- package/dist/stable-ids/diff.js +37 -0
- package/dist/stable-ids/diff.js.map +1 -0
- package/dist/stable-ids/go.d.ts +28 -0
- package/dist/stable-ids/go.d.ts.map +1 -0
- package/dist/stable-ids/go.js +160 -0
- package/dist/stable-ids/go.js.map +1 -0
- package/dist/stable-ids/hash.d.ts +22 -0
- package/dist/stable-ids/hash.d.ts.map +1 -0
- package/dist/stable-ids/hash.js +167 -0
- package/dist/stable-ids/hash.js.map +1 -0
- package/dist/stable-ids/index.d.ts +30 -0
- package/dist/stable-ids/index.d.ts.map +1 -0
- package/dist/stable-ids/index.js +59 -0
- package/dist/stable-ids/index.js.map +1 -0
- package/dist/stable-ids/java.d.ts +28 -0
- package/dist/stable-ids/java.d.ts.map +1 -0
- package/dist/stable-ids/java.js +224 -0
- package/dist/stable-ids/java.js.map +1 -0
- package/dist/stable-ids/moves.d.ts +87 -0
- package/dist/stable-ids/moves.d.ts.map +1 -0
- package/dist/stable-ids/moves.js +222 -0
- package/dist/stable-ids/moves.js.map +1 -0
- package/dist/stable-ids/nodes.d.ts +32 -0
- package/dist/stable-ids/nodes.d.ts.map +1 -0
- package/dist/stable-ids/nodes.js +92 -0
- package/dist/stable-ids/nodes.js.map +1 -0
- package/dist/stable-ids/normalize.d.ts +35 -0
- package/dist/stable-ids/normalize.d.ts.map +1 -0
- package/dist/stable-ids/normalize.js +174 -0
- package/dist/stable-ids/normalize.js.map +1 -0
- package/dist/stable-ids/parse-errors.d.ts +24 -0
- package/dist/stable-ids/parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/parse-errors.js +46 -0
- package/dist/stable-ids/parse-errors.js.map +1 -0
- package/dist/stable-ids/python.d.ts +27 -0
- package/dist/stable-ids/python.d.ts.map +1 -0
- package/dist/stable-ids/python.js +212 -0
- package/dist/stable-ids/python.js.map +1 -0
- package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
- package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/ts-parse-errors.js +75 -0
- package/dist/stable-ids/ts-parse-errors.js.map +1 -0
- package/dist/stable-ids/types.d.ts +106 -0
- package/dist/stable-ids/types.d.ts.map +1 -0
- package/dist/stable-ids/types.js +16 -0
- package/dist/stable-ids/types.js.map +1 -0
- package/dist/stable-ids/typescript.d.ts +30 -0
- package/dist/stable-ids/typescript.d.ts.map +1 -0
- package/dist/stable-ids/typescript.js +362 -0
- package/dist/stable-ids/typescript.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript call edges keyed by stable declaration ids.
|
|
3
|
+
*
|
|
4
|
+
* Every call site in a set of files becomes either an **edge** from the
|
|
5
|
+
* enclosing declaration to the declaration(s) it can run, labelled with the
|
|
6
|
+
* same tiers as the Java navigation index, or an **unresolved** site with a
|
|
7
|
+
* reason from the same closed vocabulary. Nothing is dropped silently: the
|
|
8
|
+
* two lists together account for every call and `new` expression walked.
|
|
9
|
+
*
|
|
10
|
+
* Tiers:
|
|
11
|
+
* - `exact` — the callee is a single declaration with a body, bound through
|
|
12
|
+
* lexical scope, an import, a namespace, a class name, or a receiver whose
|
|
13
|
+
* type is known (a type annotation, a `new` initializer, `this`, `super`)
|
|
14
|
+
* and whose method no subtype in the tree overrides.
|
|
15
|
+
* - `polymorphic` — the receiver's type is known and dispatch is open: the
|
|
16
|
+
* method is abstract or on an interface, or a subtype overrides it. One
|
|
17
|
+
* edge per body that could run, each carrying `via` (the declaring member).
|
|
18
|
+
* - `inferred` — a member call on a receiver of unknown type, bound by the
|
|
19
|
+
* method's name alone to every method of that name in the tree. Reported
|
|
20
|
+
* and labelled, never promoted.
|
|
21
|
+
*
|
|
22
|
+
* Bounds of this version, each surfacing as an unresolved site rather than
|
|
23
|
+
* a guess: no type inference beyond the shapes above (a call's return type is
|
|
24
|
+
* not followed); `tsconfig` path aliases only through `moduleAliases`; JSX
|
|
25
|
+
* element rendering is not a call; module-level names are importable whether
|
|
26
|
+
* or not they are exported.
|
|
27
|
+
*/
|
|
28
|
+
import { disposeTree, parse } from '../core/parser.js';
|
|
29
|
+
import { declLocator } from '../stable-ids/nodes.js';
|
|
30
|
+
import { deriveTypeScriptIds } from '../stable-ids/typescript.js';
|
|
31
|
+
import { DEFAULT_ENTRY_TABLES } from './ts-entry-tables.js';
|
|
32
|
+
export const TS_CALL_EDGES_SCHEME = 'ts-call-edges@0.1';
|
|
33
|
+
/** `methodName` of an unresolved site that loads a module by a computed specifier (`import(name)`). */
|
|
34
|
+
export const COMPUTED_MODULE_LOAD = '<module>';
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Paths
|
|
37
|
+
const EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.d.ts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
38
|
+
function normalizePath(p) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const part of p.split('/')) {
|
|
41
|
+
if (part === '' || part === '.')
|
|
42
|
+
continue;
|
|
43
|
+
if (part === '..')
|
|
44
|
+
out.pop();
|
|
45
|
+
else
|
|
46
|
+
out.push(part);
|
|
47
|
+
}
|
|
48
|
+
return out.join('/');
|
|
49
|
+
}
|
|
50
|
+
function dirname(p) {
|
|
51
|
+
const i = p.lastIndexOf('/');
|
|
52
|
+
return i < 0 ? '' : p.slice(0, i);
|
|
53
|
+
}
|
|
54
|
+
function languageOf(path) {
|
|
55
|
+
const lower = path.toLowerCase();
|
|
56
|
+
if (lower.endsWith('.tsx'))
|
|
57
|
+
return { grammar: 'tsx', ids: 'typescript' };
|
|
58
|
+
if (lower.endsWith('.jsx'))
|
|
59
|
+
return { grammar: 'tsx', ids: 'javascript' };
|
|
60
|
+
if (/\.[mc]?ts$/.test(lower))
|
|
61
|
+
return { grammar: 'typescript', ids: 'typescript' };
|
|
62
|
+
if (/\.[mc]?js$/.test(lower))
|
|
63
|
+
return { grammar: 'javascript', ids: 'javascript' };
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const BUILTIN_TYPE_SHAPES = new Set([
|
|
67
|
+
'array_type', 'tuple_type', 'literal_type', 'template_literal_type', 'function_type', 'object_type',
|
|
68
|
+
'string', 'number', 'template_string', 'array', 'object', 'regex',
|
|
69
|
+
]);
|
|
70
|
+
const FUNCTION_NODES = new Set([
|
|
71
|
+
'function_declaration', 'generator_function_declaration', 'function_expression', 'function',
|
|
72
|
+
'generator_function', 'arrow_function', 'method_definition',
|
|
73
|
+
]);
|
|
74
|
+
function named(node) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
77
|
+
const c = node.namedChild(i);
|
|
78
|
+
if (c && c.type !== 'comment')
|
|
79
|
+
out.push(c);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function stringValue(node) {
|
|
84
|
+
if (!node || node.type !== 'string')
|
|
85
|
+
return null;
|
|
86
|
+
return named(node).map(c => c.text).join('');
|
|
87
|
+
}
|
|
88
|
+
function posKey(node, kind) {
|
|
89
|
+
return `${node.startPosition.row + 1}:${node.startPosition.column}:${kind}`;
|
|
90
|
+
}
|
|
91
|
+
/** A specifier written as a string or a template without substitutions. */
|
|
92
|
+
function literalSpecifier(node) {
|
|
93
|
+
if (!node)
|
|
94
|
+
return null;
|
|
95
|
+
if (node.type === 'string')
|
|
96
|
+
return stringValue(node);
|
|
97
|
+
if (node.type === 'template_string' && !named(node).some(c => c.type === 'template_substitution')) {
|
|
98
|
+
return node.text.slice(1, -1);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A module load expression: `import(spec)` or `require(spec)`, under any
|
|
104
|
+
* `await` or parentheses. `spec` is null when the specifier is computed.
|
|
105
|
+
*/
|
|
106
|
+
function moduleLoad(node) {
|
|
107
|
+
let n = node;
|
|
108
|
+
while (n && (n.type === 'await_expression' || n.type === 'parenthesized_expression'))
|
|
109
|
+
n = named(n)[0] ?? null;
|
|
110
|
+
if (n?.type !== 'call_expression')
|
|
111
|
+
return null;
|
|
112
|
+
const fn = n.childForFieldName('function');
|
|
113
|
+
if (fn?.type !== 'import' && !(fn?.type === 'identifier' && fn.text === 'require'))
|
|
114
|
+
return null;
|
|
115
|
+
const args = n.childForFieldName('arguments');
|
|
116
|
+
const arg = args ? named(args)[0] ?? null : null;
|
|
117
|
+
return { call: n, spec: literalSpecifier(arg) };
|
|
118
|
+
}
|
|
119
|
+
function isRequire(node) {
|
|
120
|
+
if (node?.type !== 'call_expression')
|
|
121
|
+
return null;
|
|
122
|
+
const fn = node.childForFieldName('function');
|
|
123
|
+
if (fn?.type !== 'identifier' || fn.text !== 'require')
|
|
124
|
+
return null;
|
|
125
|
+
const arg = node.childForFieldName('arguments')?.namedChild(0) ?? null;
|
|
126
|
+
return arg?.type === 'string' ? arg : null;
|
|
127
|
+
}
|
|
128
|
+
/** Identifier leaves of a binding pattern. */
|
|
129
|
+
function patternNames(node, out = []) {
|
|
130
|
+
if (!node)
|
|
131
|
+
return out;
|
|
132
|
+
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier_pattern') {
|
|
133
|
+
out.push(node.text);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
if (node.type === 'pair_pattern')
|
|
137
|
+
return patternNames(node.childForFieldName('value'), out);
|
|
138
|
+
if (node.type === 'assignment_pattern')
|
|
139
|
+
return patternNames(node.childForFieldName('left'), out);
|
|
140
|
+
for (const c of named(node)) {
|
|
141
|
+
if (c.type === 'type_annotation' || c.type === 'property_identifier')
|
|
142
|
+
continue;
|
|
143
|
+
patternNames(c, out);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
const MAX_MOUNT_CHAINS = 16;
|
|
148
|
+
/** Join route segments: one `/` between parts, none trailing (except the root). */
|
|
149
|
+
export function joinRoutePath(parts) {
|
|
150
|
+
const segs = parts.flatMap(p => p.split('/')).filter(s => s !== '');
|
|
151
|
+
return '/' + segs.join('/');
|
|
152
|
+
}
|
|
153
|
+
/** Nested router paths: a segment starting with `/` restarts from the root. */
|
|
154
|
+
function composeRelative(segments) {
|
|
155
|
+
let out = [];
|
|
156
|
+
for (const s of segments) {
|
|
157
|
+
if (s.startsWith('/'))
|
|
158
|
+
out = [s];
|
|
159
|
+
else
|
|
160
|
+
out.push(s);
|
|
161
|
+
}
|
|
162
|
+
return joinRoutePath(out);
|
|
163
|
+
}
|
|
164
|
+
/** A Next.js file path to a route: groups and parallel slots dropped, `[x]` / `[...x]` / `[[...x]]` to `{x}`. */
|
|
165
|
+
function filePath(segments) {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const s of segments) {
|
|
168
|
+
if (s === '' || /^\(.*\)$/.test(s) || s.startsWith('@'))
|
|
169
|
+
continue;
|
|
170
|
+
const dyn = s.match(/^\[\[?(?:\.\.\.)?([^\]]+)\]\]?$/);
|
|
171
|
+
out.push(dyn ? `{${dyn[1]}}` : s);
|
|
172
|
+
}
|
|
173
|
+
return joinRoutePath(out);
|
|
174
|
+
}
|
|
175
|
+
class Graph {
|
|
176
|
+
options;
|
|
177
|
+
files = new Map();
|
|
178
|
+
edges = [];
|
|
179
|
+
unresolved = [];
|
|
180
|
+
sites = 0;
|
|
181
|
+
/** Type decl id → its class or interface node and file. */
|
|
182
|
+
typeNodes = new Map();
|
|
183
|
+
/** Type decl id → direct supertypes (resolved decls). */
|
|
184
|
+
supers = new Map();
|
|
185
|
+
/** Type decl id → direct subtypes. */
|
|
186
|
+
subs = new Map();
|
|
187
|
+
/** Declarations without a body: overload, interface and abstract signatures. */
|
|
188
|
+
bodiless = new Set();
|
|
189
|
+
/** Declarations that exist only for the type checker (interfaces, aliases, signatures, ambient). */
|
|
190
|
+
typeOnly = new Set();
|
|
191
|
+
/** Method declarations by name, across the tree, for `inferred`. */
|
|
192
|
+
methodsByName = new Map();
|
|
193
|
+
maxInferred;
|
|
194
|
+
references = [];
|
|
195
|
+
fieldUses = [];
|
|
196
|
+
loadEdges = [];
|
|
197
|
+
/** Fields and accessors by name (class members only). */
|
|
198
|
+
fieldsByName = new Map();
|
|
199
|
+
entries = [];
|
|
200
|
+
unresolvedRegistrations = [];
|
|
201
|
+
tables;
|
|
202
|
+
pendingRoutes = [];
|
|
203
|
+
mounts = [];
|
|
204
|
+
constructor(options) {
|
|
205
|
+
this.options = options;
|
|
206
|
+
this.maxInferred = options.maxInferred ?? 16;
|
|
207
|
+
this.tables = options.entryTables ?? DEFAULT_ENTRY_TABLES;
|
|
208
|
+
}
|
|
209
|
+
get siteCount() {
|
|
210
|
+
return this.sites;
|
|
211
|
+
}
|
|
212
|
+
// -- loading ---------------------------------------------------------------
|
|
213
|
+
add(path, tree, ids) {
|
|
214
|
+
const root = tree.rootNode;
|
|
215
|
+
const ctx = {
|
|
216
|
+
path, ids, root, module: ids.decls[0],
|
|
217
|
+
top: new Map(), topVars: new Map(), imports: new Map(), exports: new Map(),
|
|
218
|
+
defaultLocal: null, reexports: new Map(), stars: [], loads: [], esm: false,
|
|
219
|
+
locate: declLocator(ids), byStart: new Map(),
|
|
220
|
+
};
|
|
221
|
+
for (const d of ids.decls) {
|
|
222
|
+
if (d.parent === ctx.module.id) {
|
|
223
|
+
const list = ctx.top.get(d.name);
|
|
224
|
+
if (list)
|
|
225
|
+
list.push(d);
|
|
226
|
+
else
|
|
227
|
+
ctx.top.set(d.name, [d]);
|
|
228
|
+
}
|
|
229
|
+
const accessor = d.kind === 'method' && /^(static )?[gs]et /.test(d.signature);
|
|
230
|
+
if (d.kind === 'method' && !accessor) {
|
|
231
|
+
const list = this.methodsByName.get(d.name);
|
|
232
|
+
if (list)
|
|
233
|
+
list.push(d);
|
|
234
|
+
else
|
|
235
|
+
this.methodsByName.set(d.name, [d]);
|
|
236
|
+
}
|
|
237
|
+
if ((d.kind === 'field' || accessor) && d.parent !== null && d.parent !== ctx.module.id) {
|
|
238
|
+
const list = this.fieldsByName.get(d.name);
|
|
239
|
+
if (list)
|
|
240
|
+
list.push(d);
|
|
241
|
+
else
|
|
242
|
+
this.fieldsByName.set(d.name, [d]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.files.set(path, ctx);
|
|
246
|
+
}
|
|
247
|
+
/** Second pass, once every file is loaded: module tables, node positions, hierarchy. */
|
|
248
|
+
link() {
|
|
249
|
+
for (const ctx of this.sortedFiles())
|
|
250
|
+
this.scanModule(ctx);
|
|
251
|
+
for (const ctx of this.sortedFiles())
|
|
252
|
+
this.scanTypes(ctx, ctx.root);
|
|
253
|
+
for (const [id, s] of this.supers) {
|
|
254
|
+
const parents = [...(s.extends && s.extends !== 'external' ? [s.extends] : []), ...s.implements];
|
|
255
|
+
for (const p of parents) {
|
|
256
|
+
const list = this.subs.get(p.id);
|
|
257
|
+
const self = this.declById(id);
|
|
258
|
+
if (!self)
|
|
259
|
+
continue;
|
|
260
|
+
if (list)
|
|
261
|
+
list.push(self);
|
|
262
|
+
else
|
|
263
|
+
this.subs.set(p.id, [self]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
sortedFiles() {
|
|
268
|
+
return [...this.files.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
269
|
+
}
|
|
270
|
+
idIndex = new Map();
|
|
271
|
+
declById(id) {
|
|
272
|
+
if (this.idIndex.size === 0) {
|
|
273
|
+
for (const ctx of this.files.values())
|
|
274
|
+
for (const d of ctx.ids.decls)
|
|
275
|
+
this.idIndex.set(d.id, d);
|
|
276
|
+
}
|
|
277
|
+
return this.idIndex.get(id);
|
|
278
|
+
}
|
|
279
|
+
resolveModule(from, specifier) {
|
|
280
|
+
let base = null;
|
|
281
|
+
if (specifier.startsWith('./') || specifier.startsWith('../') || specifier === '.' || specifier === '..') {
|
|
282
|
+
base = normalizePath(`${dirname(from)}/${specifier}`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
for (const [prefix, target] of Object.entries(this.options.moduleAliases ?? {}).sort()) {
|
|
286
|
+
if (specifier.startsWith(prefix)) {
|
|
287
|
+
base = normalizePath(target + specifier.slice(prefix.length));
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (base === null)
|
|
293
|
+
return null;
|
|
294
|
+
const stems = [base];
|
|
295
|
+
const js = base.match(/^(.*)\.(?:m|c)?jsx?$/);
|
|
296
|
+
if (js)
|
|
297
|
+
stems.push(js[1]);
|
|
298
|
+
for (const stem of stems) {
|
|
299
|
+
if (this.files.has(stem))
|
|
300
|
+
return stem;
|
|
301
|
+
for (const ext of EXTENSIONS)
|
|
302
|
+
if (this.files.has(stem + ext))
|
|
303
|
+
return stem + ext;
|
|
304
|
+
for (const ext of EXTENSIONS)
|
|
305
|
+
if (this.files.has(`${stem}/index${ext}`))
|
|
306
|
+
return `${stem}/index${ext}`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
scanModule(ctx) {
|
|
311
|
+
for (const d of ctx.ids.decls) {
|
|
312
|
+
// Index every declaration by the position its node starts at.
|
|
313
|
+
ctx.byStart.set(`${d.start_line}:${d.start_column}:${d.kind}`, d);
|
|
314
|
+
}
|
|
315
|
+
for (const stmt of named(ctx.root))
|
|
316
|
+
this.scanStatement(ctx, stmt);
|
|
317
|
+
this.markBodiless(ctx.root, ctx);
|
|
318
|
+
}
|
|
319
|
+
markBodiless(node, ctx, ambient = false) {
|
|
320
|
+
const t = node.type;
|
|
321
|
+
if (t === 'function_signature' || t === 'method_signature' || t === 'abstract_method_signature') {
|
|
322
|
+
const kind = t === 'function_signature' ? 'function' : 'method';
|
|
323
|
+
const d = ctx.byStart.get(posKey(node, kind));
|
|
324
|
+
if (d) {
|
|
325
|
+
this.bodiless.add(d.id);
|
|
326
|
+
this.typeOnly.add(d.id);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Declarations that exist only for the type checker: nothing runs them.
|
|
330
|
+
const inAmbient = ambient || t === 'ambient_declaration';
|
|
331
|
+
const typeKinds = { interface_declaration: 'type', type_alias_declaration: 'type', property_signature: 'field' };
|
|
332
|
+
const tk = typeKinds[t];
|
|
333
|
+
if (tk || (inAmbient && t !== 'ambient_declaration')) {
|
|
334
|
+
for (const kind of tk ? [tk] : ['function', 'type', 'field', 'module', 'method']) {
|
|
335
|
+
const d = ctx.byStart.get(posKey(node, kind));
|
|
336
|
+
if (d)
|
|
337
|
+
this.typeOnly.add(d.id);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const c of named(node))
|
|
341
|
+
this.markBodiless(c, ctx, inAmbient);
|
|
342
|
+
}
|
|
343
|
+
scanStatement(ctx, stmt) {
|
|
344
|
+
const t = stmt.type;
|
|
345
|
+
if (t === 'import_statement') {
|
|
346
|
+
const spec = stringValue(stmt.childForFieldName('source'));
|
|
347
|
+
if (spec === null)
|
|
348
|
+
return;
|
|
349
|
+
const module = this.resolveModule(ctx.path, spec);
|
|
350
|
+
if (module)
|
|
351
|
+
ctx.loads.push(module);
|
|
352
|
+
const clause = named(stmt).find(c => c.type === 'import_clause');
|
|
353
|
+
if (!clause)
|
|
354
|
+
return;
|
|
355
|
+
for (const c of named(clause)) {
|
|
356
|
+
if (c.type === 'identifier')
|
|
357
|
+
ctx.imports.set(c.text, { kind: 'default', module, specifier: spec });
|
|
358
|
+
else if (c.type === 'namespace_import') {
|
|
359
|
+
const id = named(c).find(x => x.type === 'identifier');
|
|
360
|
+
if (id)
|
|
361
|
+
ctx.imports.set(id.text, { kind: 'namespace', module, specifier: spec });
|
|
362
|
+
}
|
|
363
|
+
else if (c.type === 'named_imports') {
|
|
364
|
+
for (const s of named(c)) {
|
|
365
|
+
if (s.type !== 'import_specifier')
|
|
366
|
+
continue;
|
|
367
|
+
const name = s.childForFieldName('name')?.text;
|
|
368
|
+
const alias = s.childForFieldName('alias')?.text ?? name;
|
|
369
|
+
if (name && alias)
|
|
370
|
+
ctx.imports.set(alias, name === 'default' ? { kind: 'default', module, specifier: spec } : { kind: 'named', module, name, specifier: spec });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (t === 'export_statement') {
|
|
377
|
+
ctx.esm = true;
|
|
378
|
+
const source = stringValue(stmt.childForFieldName('source'));
|
|
379
|
+
const declaration = stmt.childForFieldName('declaration');
|
|
380
|
+
const value = stmt.childForFieldName('value');
|
|
381
|
+
const clause = named(stmt).find(c => c.type === 'export_clause');
|
|
382
|
+
if (source !== null) {
|
|
383
|
+
const module = this.resolveModule(ctx.path, source);
|
|
384
|
+
if (module)
|
|
385
|
+
ctx.loads.push(module);
|
|
386
|
+
if (!clause) {
|
|
387
|
+
ctx.stars.push(module);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const s of named(clause)) {
|
|
391
|
+
const name = s.childForFieldName('name')?.text;
|
|
392
|
+
const alias = s.childForFieldName('alias')?.text ?? name;
|
|
393
|
+
if (name && alias)
|
|
394
|
+
ctx.reexports.set(alias, { module, name });
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (clause) {
|
|
399
|
+
for (const s of named(clause)) {
|
|
400
|
+
const name = s.childForFieldName('name')?.text;
|
|
401
|
+
const alias = s.childForFieldName('alias')?.text ?? name;
|
|
402
|
+
if (name && alias) {
|
|
403
|
+
if (alias === 'default')
|
|
404
|
+
ctx.defaultLocal = name;
|
|
405
|
+
else
|
|
406
|
+
ctx.exports.set(alias, name);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (declaration) {
|
|
412
|
+
let isDefault = false;
|
|
413
|
+
for (let i = 0; i < stmt.childCount; i++) {
|
|
414
|
+
const c = stmt.child(i);
|
|
415
|
+
if (c && !c.isNamed && c.text === 'default')
|
|
416
|
+
isDefault = true;
|
|
417
|
+
}
|
|
418
|
+
const nameNode = declaration.childForFieldName('name');
|
|
419
|
+
if (isDefault && nameNode)
|
|
420
|
+
ctx.defaultLocal = nameNode.text;
|
|
421
|
+
else if (nameNode)
|
|
422
|
+
ctx.exports.set(nameNode.text, nameNode.text);
|
|
423
|
+
else if (declaration.type === 'lexical_declaration' || declaration.type === 'variable_declaration') {
|
|
424
|
+
for (const d of named(declaration)) {
|
|
425
|
+
for (const n of d.type === 'variable_declarator' ? patternNames(d.childForFieldName('name')) : [])
|
|
426
|
+
ctx.exports.set(n, n);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
this.scanStatement(ctx, declaration);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (value) {
|
|
433
|
+
if (value.type === 'identifier')
|
|
434
|
+
ctx.defaultLocal = value.text;
|
|
435
|
+
else {
|
|
436
|
+
const own = value.childForFieldName('name');
|
|
437
|
+
ctx.defaultLocal = own ? own.text : 'default';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (t === 'lexical_declaration' || t === 'variable_declaration') {
|
|
443
|
+
for (const d of named(stmt)) {
|
|
444
|
+
if (d.type !== 'variable_declarator')
|
|
445
|
+
continue;
|
|
446
|
+
const nameNode = d.childForFieldName('name');
|
|
447
|
+
const value = d.childForFieldName('value');
|
|
448
|
+
const req = isRequire(value);
|
|
449
|
+
const specifier = stringValue(req) ?? '';
|
|
450
|
+
const reqModule = req ? this.resolveModule(ctx.path, specifier) : null;
|
|
451
|
+
if (reqModule)
|
|
452
|
+
ctx.loads.push(reqModule);
|
|
453
|
+
if (nameNode?.type === 'identifier') {
|
|
454
|
+
ctx.topVars.set(nameNode.text, d);
|
|
455
|
+
if (req)
|
|
456
|
+
ctx.imports.set(nameNode.text, { kind: 'namespace', module: reqModule, specifier });
|
|
457
|
+
}
|
|
458
|
+
else if (nameNode?.type === 'object_pattern' && req) {
|
|
459
|
+
for (const p of named(nameNode)) {
|
|
460
|
+
if (p.type === 'shorthand_property_identifier_pattern')
|
|
461
|
+
ctx.imports.set(p.text, { kind: 'named', module: reqModule, name: p.text, specifier });
|
|
462
|
+
else if (p.type === 'pair_pattern') {
|
|
463
|
+
const key = p.childForFieldName('key')?.text;
|
|
464
|
+
const local = p.childForFieldName('value');
|
|
465
|
+
if (key && local?.type === 'identifier')
|
|
466
|
+
ctx.imports.set(local.text, { kind: 'named', module: reqModule, name: key, specifier });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Classes and interfaces: their nodes and resolved supertypes. */
|
|
474
|
+
scanTypes(ctx, node) {
|
|
475
|
+
const t = node.type;
|
|
476
|
+
if (t === 'class_declaration' || t === 'abstract_class_declaration' || t === 'class' || t === 'interface_declaration') {
|
|
477
|
+
const decl = this.typeDeclAt(ctx, node);
|
|
478
|
+
if (decl) {
|
|
479
|
+
this.typeNodes.set(decl.id, { node, ctx });
|
|
480
|
+
let ext = null;
|
|
481
|
+
const impls = [];
|
|
482
|
+
const heritage = named(node).find(c => c.type === 'class_heritage');
|
|
483
|
+
const extendsNodes = [];
|
|
484
|
+
const implNodes = [];
|
|
485
|
+
if (heritage) {
|
|
486
|
+
for (const c of named(heritage)) {
|
|
487
|
+
if (c.type === 'extends_clause') {
|
|
488
|
+
const v = c.childForFieldName('value') ?? named(c)[0];
|
|
489
|
+
if (v)
|
|
490
|
+
extendsNodes.push(v);
|
|
491
|
+
}
|
|
492
|
+
else if (c.type === 'implements_clause')
|
|
493
|
+
implNodes.push(...named(c));
|
|
494
|
+
else
|
|
495
|
+
extendsNodes.push(c); // JavaScript: `class_heritage (identifier)`
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const ifaceExt = named(node).find(c => c.type === 'extends_type_clause');
|
|
499
|
+
if (ifaceExt)
|
|
500
|
+
implNodes.push(...named(ifaceExt));
|
|
501
|
+
if (extendsNodes[0]) {
|
|
502
|
+
const r = this.resolveTypeRef(ctx, extendsNodes[0]);
|
|
503
|
+
ext = r === null ? null : r;
|
|
504
|
+
}
|
|
505
|
+
for (const n of implNodes) {
|
|
506
|
+
const r = this.resolveTypeRef(ctx, n);
|
|
507
|
+
if (r && r !== 'external')
|
|
508
|
+
impls.push(r);
|
|
509
|
+
}
|
|
510
|
+
this.supers.set(decl.id, { extends: ext, implements: impls });
|
|
511
|
+
if (t !== 'interface_declaration')
|
|
512
|
+
this.decoratorEntries(ctx, node, decl);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const c of named(node))
|
|
516
|
+
this.scanTypes(ctx, c);
|
|
517
|
+
}
|
|
518
|
+
typeDeclAt(ctx, node) {
|
|
519
|
+
const direct = ctx.byStart.get(posKey(node, 'type'));
|
|
520
|
+
if (direct)
|
|
521
|
+
return direct;
|
|
522
|
+
// `const C = class {}` / `export default class {}`: the declaration starts at the holder.
|
|
523
|
+
let best = null;
|
|
524
|
+
const line = node.startPosition.row + 1;
|
|
525
|
+
for (const d of ctx.ids.decls) {
|
|
526
|
+
if (d.kind !== 'type')
|
|
527
|
+
continue;
|
|
528
|
+
const inside = (line > d.start_line || (line === d.start_line && node.startPosition.column >= d.start_column))
|
|
529
|
+
&& node.endPosition.row + 1 <= d.end_line;
|
|
530
|
+
if (inside && (!best || d.start_line > best.start_line || (d.start_line === best.start_line && d.start_column >= best.start_column)))
|
|
531
|
+
best = d;
|
|
532
|
+
}
|
|
533
|
+
return best;
|
|
534
|
+
}
|
|
535
|
+
// -- name resolution -------------------------------------------------------
|
|
536
|
+
resolveExport(ctx, name, depth) {
|
|
537
|
+
if (depth > 8)
|
|
538
|
+
return { kind: 'none' };
|
|
539
|
+
if (name === 'default') {
|
|
540
|
+
if (ctx.defaultLocal !== null) {
|
|
541
|
+
if (ctx.defaultLocal === 'default') {
|
|
542
|
+
const d = ctx.top.get('default');
|
|
543
|
+
return d ? { kind: 'decls', decls: d, ctx } : { kind: 'none' };
|
|
544
|
+
}
|
|
545
|
+
return this.resolveLocal(ctx, ctx.defaultLocal, depth + 1);
|
|
546
|
+
}
|
|
547
|
+
const d = ctx.top.get('default');
|
|
548
|
+
return d ? { kind: 'decls', decls: d, ctx } : { kind: 'none' };
|
|
549
|
+
}
|
|
550
|
+
const local = ctx.exports.get(name);
|
|
551
|
+
if (local !== undefined)
|
|
552
|
+
return this.resolveLocal(ctx, local, depth + 1);
|
|
553
|
+
const re = ctx.reexports.get(name);
|
|
554
|
+
if (re) {
|
|
555
|
+
const target = re.module === null ? undefined : this.files.get(re.module);
|
|
556
|
+
return target ? this.resolveExport(target, re.name, depth + 1) : { kind: 'external' };
|
|
557
|
+
}
|
|
558
|
+
if (ctx.top.has(name))
|
|
559
|
+
return this.resolveLocal(ctx, name, depth + 1);
|
|
560
|
+
for (const m of ctx.stars) {
|
|
561
|
+
const target = m === null ? undefined : this.files.get(m);
|
|
562
|
+
if (!target)
|
|
563
|
+
continue;
|
|
564
|
+
const r = this.resolveExport(target, name, depth + 1);
|
|
565
|
+
if (r.kind !== 'none')
|
|
566
|
+
return r;
|
|
567
|
+
}
|
|
568
|
+
return { kind: 'none' };
|
|
569
|
+
}
|
|
570
|
+
/** A module-level name: a declaration here, or an import followed to its file. */
|
|
571
|
+
resolveLocal(ctx, name, depth = 0) {
|
|
572
|
+
// An import wins: `const a = require('./a')` is also a module-level field.
|
|
573
|
+
const imp = ctx.imports.get(name);
|
|
574
|
+
if (!imp) {
|
|
575
|
+
const decls = ctx.top.get(name);
|
|
576
|
+
return decls ? { kind: 'decls', decls, ctx } : { kind: 'none' };
|
|
577
|
+
}
|
|
578
|
+
return this.resolveBinding(imp, depth);
|
|
579
|
+
}
|
|
580
|
+
/** What an import binding denotes: a namespace, or the export it names. */
|
|
581
|
+
resolveBinding(imp, depth = 0) {
|
|
582
|
+
const target = imp.module === null ? undefined : this.files.get(imp.module);
|
|
583
|
+
if (!target)
|
|
584
|
+
return { kind: 'external' };
|
|
585
|
+
if (imp.kind === 'namespace')
|
|
586
|
+
return { kind: 'namespace', ctx: target };
|
|
587
|
+
return this.resolveExport(target, imp.kind === 'default' ? 'default' : imp.name, depth + 1);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* A local variable bound to a module load with a literal specifier:
|
|
591
|
+
* `const m = await import('./m')` is the namespace, `const { f } = await
|
|
592
|
+
* import('./m')` and `const { f } = require('./m')` name the export.
|
|
593
|
+
*/
|
|
594
|
+
loadBinding(ctx, declarator, name) {
|
|
595
|
+
const load = moduleLoad(declarator.childForFieldName('value'));
|
|
596
|
+
if (!load || load.spec === null)
|
|
597
|
+
return null;
|
|
598
|
+
const specifier = load.spec;
|
|
599
|
+
const module = this.resolveModule(ctx.path, specifier);
|
|
600
|
+
const pattern = declarator.childForFieldName('name');
|
|
601
|
+
if (pattern?.type === 'identifier')
|
|
602
|
+
return pattern.text === name ? { kind: 'namespace', module, specifier } : null;
|
|
603
|
+
if (pattern?.type !== 'object_pattern')
|
|
604
|
+
return null;
|
|
605
|
+
for (const p of named(pattern)) {
|
|
606
|
+
let key;
|
|
607
|
+
if (p.type === 'shorthand_property_identifier_pattern' && p.text === name)
|
|
608
|
+
key = name;
|
|
609
|
+
else if (p.type === 'pair_pattern' && p.childForFieldName('value')?.text === name)
|
|
610
|
+
key = p.childForFieldName('key')?.text;
|
|
611
|
+
if (key === 'default')
|
|
612
|
+
return { kind: 'default', module, specifier };
|
|
613
|
+
if (key)
|
|
614
|
+
return { kind: 'named', module, name: key, specifier };
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
resolveTypeRef(ctx, node) {
|
|
619
|
+
let n = node;
|
|
620
|
+
if (n.type === 'type_annotation')
|
|
621
|
+
n = named(n)[0] ?? null;
|
|
622
|
+
if (n?.type === 'generic_type')
|
|
623
|
+
n = n.childForFieldName('name') ?? named(n)[0] ?? null;
|
|
624
|
+
if (!n)
|
|
625
|
+
return null;
|
|
626
|
+
// Built-in shapes have no declaration in the tree; `any` and `unknown` say nothing.
|
|
627
|
+
if (n.type === 'predefined_type')
|
|
628
|
+
return n.text === 'any' || n.text === 'unknown' ? null : 'external';
|
|
629
|
+
if (BUILTIN_TYPE_SHAPES.has(n.type))
|
|
630
|
+
return 'external';
|
|
631
|
+
if (n.type === 'nested_type_identifier' || n.type === 'member_expression') {
|
|
632
|
+
const parts = n.text.split('.');
|
|
633
|
+
if (parts.length !== 2)
|
|
634
|
+
return 'external';
|
|
635
|
+
const ns = this.resolveLocal(ctx, parts[0]);
|
|
636
|
+
if (ns.kind !== 'namespace')
|
|
637
|
+
return 'external';
|
|
638
|
+
const r = this.resolveExport(ns.ctx, parts[1], 0);
|
|
639
|
+
return r.kind === 'decls' ? (r.decls.find(d => d.kind === 'type') ?? 'external') : 'external';
|
|
640
|
+
}
|
|
641
|
+
if (n.type !== 'type_identifier' && n.type !== 'identifier')
|
|
642
|
+
return null;
|
|
643
|
+
const r = this.resolveLocal(ctx, n.text);
|
|
644
|
+
if (r.kind === 'decls')
|
|
645
|
+
return r.decls.find(d => d.kind === 'type') ?? null;
|
|
646
|
+
return 'external';
|
|
647
|
+
}
|
|
648
|
+
// -- lexical scope ---------------------------------------------------------
|
|
649
|
+
/**
|
|
650
|
+
* The nearest binding of `name` inside the enclosing functions and blocks
|
|
651
|
+
* (module level excluded): a parameter, a variable declarator, or a nested
|
|
652
|
+
* function declaration.
|
|
653
|
+
*/
|
|
654
|
+
localBinding(start, name) {
|
|
655
|
+
for (let n = start.parent; n && n.type !== 'program'; n = n.parent) {
|
|
656
|
+
if (FUNCTION_NODES.has(n.type)) {
|
|
657
|
+
const params = n.childForFieldName('parameters');
|
|
658
|
+
const single = n.childForFieldName('parameter');
|
|
659
|
+
if (single && single.text === name)
|
|
660
|
+
return { kind: 'param', node: single };
|
|
661
|
+
if (params) {
|
|
662
|
+
for (const p of named(params)) {
|
|
663
|
+
const pattern = p.childForFieldName('pattern') ?? p;
|
|
664
|
+
if (patternNames(pattern).includes(name))
|
|
665
|
+
return { kind: 'param', node: p };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (n.type === 'catch_clause' && patternNames(n.childForFieldName('parameter')).includes(name)) {
|
|
670
|
+
return { kind: 'param', node: n };
|
|
671
|
+
}
|
|
672
|
+
if (n.type === 'statement_block' || n.type === 'for_statement' || n.type === 'for_in_statement') {
|
|
673
|
+
for (const s of named(n)) {
|
|
674
|
+
if ((s.type === 'function_declaration' || s.type === 'generator_function_declaration') && s.childForFieldName('name')?.text === name) {
|
|
675
|
+
return { kind: 'fn', node: s };
|
|
676
|
+
}
|
|
677
|
+
if (s.type === 'lexical_declaration' || s.type === 'variable_declaration') {
|
|
678
|
+
for (const d of named(s)) {
|
|
679
|
+
if (d.type === 'variable_declarator' && patternNames(d.childForFieldName('name')).includes(name))
|
|
680
|
+
return { kind: 'var', node: d };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (n.type === 'for_in_statement' && patternNames(n.childForFieldName('left')).includes(name))
|
|
685
|
+
return { kind: 'param', node: n };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
nestedDeclAt(ctx, node) {
|
|
691
|
+
return ctx.byStart.get(posKey(node, 'function')) ?? null;
|
|
692
|
+
}
|
|
693
|
+
// -- receiver types --------------------------------------------------------
|
|
694
|
+
enclosingClass(ctx, node) {
|
|
695
|
+
for (let n = node.parent; n; n = n.parent) {
|
|
696
|
+
// A non-arrow function rebinds `this`; methods are the exception.
|
|
697
|
+
if ((n.type === 'function_expression' || n.type === 'function' || n.type === 'function_declaration') && n.parent?.type !== 'class_body')
|
|
698
|
+
return null;
|
|
699
|
+
if (n.type === 'class_declaration' || n.type === 'abstract_class_declaration' || n.type === 'class')
|
|
700
|
+
return this.typeDeclAt(ctx, n);
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
typeOfVar(ctx, declarator) {
|
|
705
|
+
const type = declarator.childForFieldName('type');
|
|
706
|
+
if (type)
|
|
707
|
+
return this.resolveTypeRef(ctx, type);
|
|
708
|
+
const value = declarator.childForFieldName('value');
|
|
709
|
+
if (value?.type === 'new_expression') {
|
|
710
|
+
const c = value.childForFieldName('constructor');
|
|
711
|
+
return c ? this.resolveTypeRef(ctx, c) : null;
|
|
712
|
+
}
|
|
713
|
+
// A literal's methods are the language's own.
|
|
714
|
+
if (value && BUILTIN_TYPE_SHAPES.has(value.type))
|
|
715
|
+
return 'external';
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
/** The class's own field or constructor parameter property named `name`, typed. */
|
|
719
|
+
typeOfField(cls, name) {
|
|
720
|
+
const entry = this.typeNodes.get(cls.id);
|
|
721
|
+
if (!entry)
|
|
722
|
+
return null;
|
|
723
|
+
const body = entry.node.childForFieldName('body');
|
|
724
|
+
if (!body)
|
|
725
|
+
return null;
|
|
726
|
+
for (const m of named(body)) {
|
|
727
|
+
if ((m.type === 'public_field_definition' || m.type === 'field_definition') && (m.childForFieldName('name') ?? m.childForFieldName('property'))?.text === name) {
|
|
728
|
+
const type = m.childForFieldName('type');
|
|
729
|
+
if (type)
|
|
730
|
+
return this.resolveTypeRef(entry.ctx, type);
|
|
731
|
+
const value = m.childForFieldName('value');
|
|
732
|
+
if (value?.type === 'new_expression') {
|
|
733
|
+
const c = value.childForFieldName('constructor');
|
|
734
|
+
return c ? this.resolveTypeRef(entry.ctx, c) : null;
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
if (m.type === 'method_definition' && m.childForFieldName('name')?.text === 'constructor') {
|
|
739
|
+
const params = m.childForFieldName('parameters');
|
|
740
|
+
for (const p of params ? named(params) : []) {
|
|
741
|
+
const hasModifier = named(p).some(c => c.type === 'accessibility_modifier' || c.type === 'override_modifier')
|
|
742
|
+
|| /^\s*readonly\b/.test(p.text);
|
|
743
|
+
if (!hasModifier || p.childForFieldName('pattern')?.text !== name)
|
|
744
|
+
continue;
|
|
745
|
+
const type = p.childForFieldName('type');
|
|
746
|
+
return type ? this.resolveTypeRef(entry.ctx, type) : null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const sup = this.supers.get(cls.id)?.extends;
|
|
751
|
+
return sup && sup !== 'external' ? this.typeOfField(sup, name) : null;
|
|
752
|
+
}
|
|
753
|
+
typeOf(ctx, expr) {
|
|
754
|
+
if (expr.type === 'this')
|
|
755
|
+
return this.enclosingClass(ctx, expr);
|
|
756
|
+
if (BUILTIN_TYPE_SHAPES.has(expr.type))
|
|
757
|
+
return 'external';
|
|
758
|
+
if (expr.type === 'parenthesized_expression' || expr.type === 'non_null_expression') {
|
|
759
|
+
const inner = named(expr)[0];
|
|
760
|
+
return inner ? this.typeOf(ctx, inner) : null;
|
|
761
|
+
}
|
|
762
|
+
if (expr.type === 'new_expression') {
|
|
763
|
+
const c = expr.childForFieldName('constructor');
|
|
764
|
+
return c ? this.resolveTypeRef(ctx, c) : null;
|
|
765
|
+
}
|
|
766
|
+
if (expr.type === 'identifier') {
|
|
767
|
+
const b = this.localBinding(expr, expr.text);
|
|
768
|
+
if (b?.kind === 'param') {
|
|
769
|
+
const type = b.node.childForFieldName('type');
|
|
770
|
+
if (type)
|
|
771
|
+
return this.resolveTypeRef(ctx, type);
|
|
772
|
+
const value = b.node.childForFieldName('value') ?? b.node.childForFieldName('right');
|
|
773
|
+
const c = value?.type === 'new_expression' ? value.childForFieldName('constructor') : null;
|
|
774
|
+
return c ? this.resolveTypeRef(ctx, c) : null;
|
|
775
|
+
}
|
|
776
|
+
if (b?.kind === 'var')
|
|
777
|
+
return this.typeOfVar(ctx, b.node);
|
|
778
|
+
if (b)
|
|
779
|
+
return null;
|
|
780
|
+
const top = ctx.topVars.get(expr.text);
|
|
781
|
+
return top ? this.typeOfVar(ctx, top) : null;
|
|
782
|
+
}
|
|
783
|
+
if (expr.type === 'member_expression') {
|
|
784
|
+
const object = expr.childForFieldName('object');
|
|
785
|
+
const property = expr.childForFieldName('property');
|
|
786
|
+
if (object?.type === 'this' && property) {
|
|
787
|
+
const cls = this.enclosingClass(ctx, expr);
|
|
788
|
+
return cls ? this.typeOfField(cls, property.text) : null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
// -- members and dispatch --------------------------------------------------
|
|
794
|
+
/** Methods named `name` declared on `type` or inherited through `extends`. */
|
|
795
|
+
findMember(type, name, seen = new Set()) {
|
|
796
|
+
if (seen.has(type.id))
|
|
797
|
+
return null;
|
|
798
|
+
seen.add(type.id);
|
|
799
|
+
const own = this.membersOf(type, name);
|
|
800
|
+
if (own.length > 0)
|
|
801
|
+
return { decls: own, owner: type };
|
|
802
|
+
const s = this.supers.get(type.id);
|
|
803
|
+
if (s?.extends === 'external')
|
|
804
|
+
return 'external';
|
|
805
|
+
if (s?.extends) {
|
|
806
|
+
const r = this.findMember(s.extends, name, seen);
|
|
807
|
+
if (r)
|
|
808
|
+
return r;
|
|
809
|
+
}
|
|
810
|
+
for (const i of s?.implements ?? []) {
|
|
811
|
+
const r = this.findMember(i, name, seen);
|
|
812
|
+
if (r && r !== 'external')
|
|
813
|
+
return r;
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
membersOf(type, name) {
|
|
818
|
+
return (this.methodsByName.get(name) ?? []).filter(d => d.parent === type.id);
|
|
819
|
+
}
|
|
820
|
+
/** Bodies overriding `name` in every subtype of `type`, sorted by id. */
|
|
821
|
+
overrides(type, name) {
|
|
822
|
+
const out = new Map();
|
|
823
|
+
const walk = (t, seen) => {
|
|
824
|
+
for (const s of this.subs.get(t.id) ?? []) {
|
|
825
|
+
if (seen.has(s.id))
|
|
826
|
+
continue;
|
|
827
|
+
seen.add(s.id);
|
|
828
|
+
for (const m of this.membersOf(s, name))
|
|
829
|
+
if (!this.bodiless.has(m.id))
|
|
830
|
+
out.set(m.id, m);
|
|
831
|
+
walk(s, seen);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
walk(type, new Set([type.id]));
|
|
835
|
+
return [...out.values()].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
836
|
+
}
|
|
837
|
+
// -- sites -----------------------------------------------------------------
|
|
838
|
+
walk(ctx, node) {
|
|
839
|
+
const t = node.type;
|
|
840
|
+
if (t === 'member_expression' && node.childForFieldName('object')?.type === 'this')
|
|
841
|
+
this.fieldUse(ctx, node);
|
|
842
|
+
if (t === 'call_expression' || t === 'new_expression') {
|
|
843
|
+
this.site(ctx, node);
|
|
844
|
+
if (t === 'call_expression') {
|
|
845
|
+
this.registration(ctx, node);
|
|
846
|
+
this.reactRouterFactory(ctx, node);
|
|
847
|
+
}
|
|
848
|
+
const args = node.childForFieldName('arguments');
|
|
849
|
+
if (args?.type === 'arguments')
|
|
850
|
+
for (const a of named(args))
|
|
851
|
+
this.reference(ctx, a);
|
|
852
|
+
}
|
|
853
|
+
else if (t === 'pair')
|
|
854
|
+
this.reference(ctx, node.childForFieldName('value'));
|
|
855
|
+
else if (t === 'shorthand_property_identifier')
|
|
856
|
+
this.reference(ctx, node);
|
|
857
|
+
else if (t === 'array')
|
|
858
|
+
for (const c of named(node))
|
|
859
|
+
this.reference(ctx, c);
|
|
860
|
+
else if (t === 'return_statement')
|
|
861
|
+
this.reference(ctx, named(node)[0] ?? null);
|
|
862
|
+
else if (t === 'arrow_function') {
|
|
863
|
+
const body = node.childForFieldName('body');
|
|
864
|
+
if (body && body.type !== 'statement_block')
|
|
865
|
+
this.reference(ctx, body);
|
|
866
|
+
}
|
|
867
|
+
else if (t === 'assignment_expression')
|
|
868
|
+
this.reference(ctx, node.childForFieldName('right'));
|
|
869
|
+
else if (t === 'variable_declarator')
|
|
870
|
+
this.reference(ctx, node.childForFieldName('value'));
|
|
871
|
+
else if (t === 'export_statement')
|
|
872
|
+
this.reference(ctx, node.childForFieldName('value'));
|
|
873
|
+
else if (t === 'jsx_opening_element' || t === 'jsx_self_closing_element')
|
|
874
|
+
this.reference(ctx, node.childForFieldName('name'));
|
|
875
|
+
if (t === 'jsx_element' || t === 'jsx_self_closing_element')
|
|
876
|
+
this.reactRouteElement(ctx, node);
|
|
877
|
+
else if (t === 'jsx_expression')
|
|
878
|
+
this.reference(ctx, named(node)[0] ?? null);
|
|
879
|
+
for (const c of named(node))
|
|
880
|
+
this.walk(ctx, c);
|
|
881
|
+
}
|
|
882
|
+
// -- field uses (`this.x`) -------------------------------------------------
|
|
883
|
+
/** `this.x` in a class body: the class's own or inherited field (or accessor) `x` is in use. */
|
|
884
|
+
fieldUse(ctx, node) {
|
|
885
|
+
const name = node.childForFieldName('property')?.text;
|
|
886
|
+
if (!name)
|
|
887
|
+
return;
|
|
888
|
+
const candidates = this.fieldsByName.get(name);
|
|
889
|
+
if (!candidates)
|
|
890
|
+
return;
|
|
891
|
+
const cls = this.enclosingClass(ctx, node);
|
|
892
|
+
if (!cls)
|
|
893
|
+
return;
|
|
894
|
+
const targets = [];
|
|
895
|
+
const seen = new Set();
|
|
896
|
+
for (let t = cls; t && !seen.has(t.id);) {
|
|
897
|
+
seen.add(t.id);
|
|
898
|
+
const own = candidates.filter(d => d.parent === t.id);
|
|
899
|
+
if (own.length > 0) {
|
|
900
|
+
targets.push(...own);
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
const s = this.supers.get(t.id)?.extends;
|
|
904
|
+
t = s && s !== 'external' ? s : null;
|
|
905
|
+
}
|
|
906
|
+
if (targets.length === 0)
|
|
907
|
+
return;
|
|
908
|
+
const site = { file: ctx.path, line: node.startPosition.row + 1, col: node.startPosition.column };
|
|
909
|
+
const from = (ctx.locate(site.line, site.col) ?? ctx.module).id;
|
|
910
|
+
for (const d of targets)
|
|
911
|
+
if (d.id !== from)
|
|
912
|
+
this.fieldUses.push({ from, to: d.id, site });
|
|
913
|
+
}
|
|
914
|
+
// -- references (functions and classes used as values) ---------------------
|
|
915
|
+
reference(ctx, node) {
|
|
916
|
+
if (!node)
|
|
917
|
+
return;
|
|
918
|
+
if (node.type === 'parenthesized_expression' || node.type === 'as_expression' || node.type === 'non_null_expression' || node.type === 'satisfies_expression') {
|
|
919
|
+
this.reference(ctx, named(node)[0] ?? null);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const targets = this.valueTargets(ctx, node);
|
|
923
|
+
if (targets.length === 0)
|
|
924
|
+
return;
|
|
925
|
+
const site = { file: ctx.path, line: node.startPosition.row + 1, col: node.startPosition.column };
|
|
926
|
+
const from = (ctx.locate(site.line, site.col) ?? ctx.module).id;
|
|
927
|
+
for (const d of targets)
|
|
928
|
+
if (d.id !== from)
|
|
929
|
+
this.references.push({ from, to: d.id, site });
|
|
930
|
+
}
|
|
931
|
+
callables(decls) {
|
|
932
|
+
return decls.filter(d => (d.kind === 'function' || d.kind === 'type' || d.kind === 'method') && !this.bodiless.has(d.id));
|
|
933
|
+
}
|
|
934
|
+
memberBodies(type, name) {
|
|
935
|
+
const found = this.findMember(type, name);
|
|
936
|
+
if (!found || found === 'external')
|
|
937
|
+
return [];
|
|
938
|
+
return [...this.callables(found.decls), ...this.overrides(found.owner, name)];
|
|
939
|
+
}
|
|
940
|
+
/** The declarations a value expression denotes: a function, a class or a method. */
|
|
941
|
+
valueTargets(ctx, node) {
|
|
942
|
+
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier') {
|
|
943
|
+
const name = node.text;
|
|
944
|
+
const local = this.localBinding(node, name);
|
|
945
|
+
if (local) {
|
|
946
|
+
const loaded = local.kind === 'var' ? this.loadBinding(ctx, local.node, name) : null;
|
|
947
|
+
if (loaded) {
|
|
948
|
+
const t = this.resolveBinding(loaded);
|
|
949
|
+
return t.kind === 'decls' ? this.callables(t.decls) : [];
|
|
950
|
+
}
|
|
951
|
+
const holder = local.kind === 'var' ? local.node.childForFieldName('value') : local.kind === 'fn' ? local.node : null;
|
|
952
|
+
const d = holder && FUNCTION_NODES.has(holder.type) ? this.nestedDeclAt(ctx, holder) : null;
|
|
953
|
+
return d ? [d] : [];
|
|
954
|
+
}
|
|
955
|
+
const t = this.resolveLocal(ctx, name);
|
|
956
|
+
return t.kind === 'decls' ? this.callables(t.decls) : [];
|
|
957
|
+
}
|
|
958
|
+
if (node.type !== 'member_expression')
|
|
959
|
+
return [];
|
|
960
|
+
const object = node.childForFieldName('object');
|
|
961
|
+
const name = node.childForFieldName('property')?.text;
|
|
962
|
+
if (!object || !name)
|
|
963
|
+
return [];
|
|
964
|
+
if (object.type === 'this') {
|
|
965
|
+
const cls = this.enclosingClass(ctx, node);
|
|
966
|
+
return cls ? this.memberBodies(cls, name) : [];
|
|
967
|
+
}
|
|
968
|
+
if (object.type === 'identifier' && !this.localBinding(object, object.text)) {
|
|
969
|
+
const t = this.resolveLocal(ctx, object.text);
|
|
970
|
+
if (t.kind === 'namespace') {
|
|
971
|
+
const m = this.resolveExport(t.ctx, name, 0);
|
|
972
|
+
return m.kind === 'decls' ? this.callables(m.decls) : [];
|
|
973
|
+
}
|
|
974
|
+
const type = t.kind === 'decls' ? t.decls.find(d => d.kind === 'type') : undefined;
|
|
975
|
+
if (type)
|
|
976
|
+
return this.memberBodies(type, name);
|
|
977
|
+
}
|
|
978
|
+
const type = this.typeOf(ctx, object);
|
|
979
|
+
return type && type !== 'external' ? this.memberBodies(type, name) : [];
|
|
980
|
+
}
|
|
981
|
+
// -- entry points ----------------------------------------------------------
|
|
982
|
+
entry(d, kind, certainty, file, line, evidence, extra = {}) {
|
|
983
|
+
this.entries.push({ id: d.id, kind, certainty, file, line, evidence, ...extra });
|
|
984
|
+
}
|
|
985
|
+
/** Route registrations (`app.get('/x', h)`) and test-runner callbacks at a call site. */
|
|
986
|
+
registration(ctx, node) {
|
|
987
|
+
const fn = node.childForFieldName('function');
|
|
988
|
+
const argsNode = node.childForFieldName('arguments');
|
|
989
|
+
const args = argsNode ? named(argsNode) : [];
|
|
990
|
+
if (!fn)
|
|
991
|
+
return;
|
|
992
|
+
const line = node.startPosition.row + 1;
|
|
993
|
+
if (this.tables.tests.filePattern.test(ctx.path)) {
|
|
994
|
+
const runnerNode = fn.type === 'identifier' ? fn : fn.type === 'member_expression' ? fn.childForFieldName('object') : null;
|
|
995
|
+
const runner = runnerNode?.type === 'identifier' ? runnerNode.text : null;
|
|
996
|
+
if (runner && runnerNode && this.tables.tests.functions.includes(runner) && !this.localBinding(runnerNode, runner)
|
|
997
|
+
&& this.resolveLocal(ctx, runner).kind !== 'decls') {
|
|
998
|
+
for (const a of args) {
|
|
999
|
+
const d = FUNCTION_NODES.has(a.type) ? this.nestedDeclAt(ctx, a) : null;
|
|
1000
|
+
if (d)
|
|
1001
|
+
this.entry(d, 'test', 'conventional', ctx.path, a.startPosition.row + 1, `${runner} callback in a test file`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (fn.type !== 'member_expression')
|
|
1006
|
+
return;
|
|
1007
|
+
const verb = fn.childForFieldName('property')?.text;
|
|
1008
|
+
if (!verb)
|
|
1009
|
+
return;
|
|
1010
|
+
let receiver = fn.childForFieldName('object');
|
|
1011
|
+
const routes = this.tables.routes;
|
|
1012
|
+
if ((routes.mountVerbs ?? []).includes(verb)) {
|
|
1013
|
+
this.mount(ctx, receiver, args, line);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (!routes.verbs.includes(verb))
|
|
1017
|
+
return;
|
|
1018
|
+
let path = stringValue(args[0] ?? null);
|
|
1019
|
+
let handlers = args.slice(1);
|
|
1020
|
+
// `router.route('/x').get(a).post(b)`: step back over earlier verbs to the scoping call.
|
|
1021
|
+
while (receiver?.type === 'call_expression') {
|
|
1022
|
+
const prev = receiver.childForFieldName('function');
|
|
1023
|
+
if (prev?.type !== 'member_expression' || !routes.verbs.includes(prev.childForFieldName('property')?.text ?? ''))
|
|
1024
|
+
break;
|
|
1025
|
+
const inner = prev.childForFieldName('object');
|
|
1026
|
+
if (inner?.type !== 'call_expression')
|
|
1027
|
+
break;
|
|
1028
|
+
receiver = inner;
|
|
1029
|
+
}
|
|
1030
|
+
if (receiver?.type === 'call_expression' && routes.routeChain) {
|
|
1031
|
+
const scope = receiver.childForFieldName('function');
|
|
1032
|
+
const scopeArgs = receiver.childForFieldName('arguments');
|
|
1033
|
+
if (scope?.type === 'member_expression' && scope.childForFieldName('property')?.text === routes.routeChain && scopeArgs) {
|
|
1034
|
+
path = stringValue(named(scopeArgs)[0] ?? null);
|
|
1035
|
+
handlers = args;
|
|
1036
|
+
receiver = scope.childForFieldName('object');
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (path === null || !(path.startsWith('/') || path === '*'))
|
|
1040
|
+
return;
|
|
1041
|
+
const origin = this.routerOrigin(ctx, receiver, 0);
|
|
1042
|
+
for (const h of handlers) {
|
|
1043
|
+
const targets = FUNCTION_NODES.has(h.type)
|
|
1044
|
+
? [this.nestedDeclAt(ctx, h)].filter((d) => d !== null)
|
|
1045
|
+
: this.valueTargets(ctx, h);
|
|
1046
|
+
if (targets.length === 0) {
|
|
1047
|
+
this.unresolvedRegistrations.push({
|
|
1048
|
+
site: { file: ctx.path, line: h.startPosition.row + 1, col: h.startPosition.column },
|
|
1049
|
+
reason: 'handler-unresolved',
|
|
1050
|
+
evidence: `${verb.toUpperCase()} ${path}: handler ${h.text.slice(0, 40)} not tied to a declaration`,
|
|
1051
|
+
});
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
for (const d of targets) {
|
|
1055
|
+
this.pendingRoutes.push({
|
|
1056
|
+
decl: d, certainty: origin.certainty, framework: origin.framework, method: verb.toUpperCase(), path,
|
|
1057
|
+
file: ctx.path, line, router: origin.key, surface: undefined,
|
|
1058
|
+
evidence: origin.framework ? `${origin.framework} route registration` : 'route-shaped registration on a receiver of unknown origin',
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* `X.use([prefix,] ...items)`: a router item is mounted under the prefix;
|
|
1065
|
+
* a function item is middleware for every route under it.
|
|
1066
|
+
*/
|
|
1067
|
+
mount(ctx, receiver, args, line) {
|
|
1068
|
+
const prefix = stringValue(args[0] ?? null);
|
|
1069
|
+
if (prefix !== null && !prefix.startsWith('/'))
|
|
1070
|
+
return;
|
|
1071
|
+
const items = prefix === null ? args : args.slice(1);
|
|
1072
|
+
if (items.length === 0)
|
|
1073
|
+
return;
|
|
1074
|
+
const parent = this.routerOrigin(ctx, receiver, 0);
|
|
1075
|
+
// Only a receiver that is itself a router of a known framework mounts anything.
|
|
1076
|
+
if (parent.certainty !== 'declared' || !parent.key)
|
|
1077
|
+
return;
|
|
1078
|
+
for (const item of items) {
|
|
1079
|
+
const child = item.type === 'identifier' || item.type === 'member_expression' ? this.routerOrigin(ctx, item, 0) : null;
|
|
1080
|
+
if (child?.certainty === 'declared' && child.key) {
|
|
1081
|
+
this.mounts.push({ parent: parent.key, child: child.key, prefix: prefix ?? '' });
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
// `app.use(cors())`: a factory's call is an ordinary call site, and the
|
|
1085
|
+
// function it returns is contained in the factory's body.
|
|
1086
|
+
if (item.type === 'call_expression' || item.type === 'new_expression')
|
|
1087
|
+
continue;
|
|
1088
|
+
const targets = FUNCTION_NODES.has(item.type)
|
|
1089
|
+
? [this.nestedDeclAt(ctx, item)].filter((d) => d !== null)
|
|
1090
|
+
: this.valueTargets(ctx, item);
|
|
1091
|
+
if (targets.length === 0) {
|
|
1092
|
+
this.unresolvedRegistrations.push({
|
|
1093
|
+
site: { file: ctx.path, line: item.startPosition.row + 1, col: item.startPosition.column },
|
|
1094
|
+
reason: 'mount-unresolved',
|
|
1095
|
+
evidence: `use ${prefix ?? ''}: ${item.text.slice(0, 40)} is neither a known router nor a declared function`,
|
|
1096
|
+
});
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
for (const d of targets) {
|
|
1100
|
+
this.pendingRoutes.push({
|
|
1101
|
+
decl: d, certainty: 'declared', framework: parent.framework, method: 'USE', path: prefix ?? '/',
|
|
1102
|
+
file: ctx.path, line, router: parent.key, surface: 'middleware', evidence: `${parent.framework} middleware`,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/** Every chain of mount prefixes from a root router down to `key`, outermost first. */
|
|
1108
|
+
mountChains(key, seen = new Set()) {
|
|
1109
|
+
if (key === undefined || seen.has(key))
|
|
1110
|
+
return [[]];
|
|
1111
|
+
const parents = this.mounts.filter(m => m.child === key);
|
|
1112
|
+
if (parents.length === 0)
|
|
1113
|
+
return [[]];
|
|
1114
|
+
const next = new Set(seen).add(key);
|
|
1115
|
+
const out = [];
|
|
1116
|
+
for (const m of parents) {
|
|
1117
|
+
for (const chain of this.mountChains(m.parent, next)) {
|
|
1118
|
+
out.push(m.prefix === '' ? chain : [...chain, m.prefix]);
|
|
1119
|
+
if (out.length >= MAX_MOUNT_CHAINS)
|
|
1120
|
+
return out;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return out;
|
|
1124
|
+
}
|
|
1125
|
+
/** Routes with their mount chains composed into full paths. */
|
|
1126
|
+
composeRoutes() {
|
|
1127
|
+
for (const r of this.pendingRoutes) {
|
|
1128
|
+
for (const chain of this.mountChains(r.router)) {
|
|
1129
|
+
const extra = { method: r.method, path: joinRoutePath([...chain, r.path]) };
|
|
1130
|
+
if (chain.length > 0) {
|
|
1131
|
+
extra.registeredPath = r.path;
|
|
1132
|
+
extra.mounts = chain;
|
|
1133
|
+
}
|
|
1134
|
+
if (r.framework)
|
|
1135
|
+
extra.framework = r.framework;
|
|
1136
|
+
if (r.surface)
|
|
1137
|
+
extra.surface = r.surface;
|
|
1138
|
+
this.entry(r.decl, 'route', r.certainty, r.file, r.line, r.evidence, extra);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// -- React Router ------------------------------------------------------------
|
|
1143
|
+
reactRouterPackage(ctx, name) {
|
|
1144
|
+
const table = this.tables.reactRouter;
|
|
1145
|
+
const imp = ctx.imports.get(name);
|
|
1146
|
+
if (!table || !imp || imp.module !== null || !table.packages.includes(imp.specifier))
|
|
1147
|
+
return null;
|
|
1148
|
+
return imp.specifier;
|
|
1149
|
+
}
|
|
1150
|
+
jsxName(el) {
|
|
1151
|
+
const opening = el.type === 'jsx_element' ? named(el).find(c => c.type === 'jsx_opening_element') ?? null : el;
|
|
1152
|
+
return opening?.childForFieldName('name') ?? null;
|
|
1153
|
+
}
|
|
1154
|
+
jsxAttributes(el) {
|
|
1155
|
+
const opening = el.type === 'jsx_element' ? named(el).find(c => c.type === 'jsx_opening_element') : el;
|
|
1156
|
+
const out = new Map();
|
|
1157
|
+
for (const a of opening ? named(opening) : []) {
|
|
1158
|
+
if (a.type !== 'jsx_attribute')
|
|
1159
|
+
continue;
|
|
1160
|
+
const parts = named(a);
|
|
1161
|
+
const key = parts[0]?.text;
|
|
1162
|
+
if (key)
|
|
1163
|
+
out.set(key, parts[1] ?? null);
|
|
1164
|
+
}
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
/** The `path` of a `<Route>` element, `''` for an index route, `null` when not a Route. */
|
|
1168
|
+
routeElementPath(ctx, el) {
|
|
1169
|
+
const name = this.jsxName(el);
|
|
1170
|
+
const table = this.tables.reactRouter;
|
|
1171
|
+
if (name?.type !== 'identifier' || !table || !table.routeElements.includes(name.text))
|
|
1172
|
+
return null;
|
|
1173
|
+
const pkg = this.reactRouterPackage(ctx, name.text);
|
|
1174
|
+
if (!pkg)
|
|
1175
|
+
return null;
|
|
1176
|
+
const attrs = this.jsxAttributes(el);
|
|
1177
|
+
const p = attrs.get('path');
|
|
1178
|
+
const path = p ? stringValue(p) ?? (p.type === 'jsx_expression' ? stringValue(named(p)[0] ?? null) : null) : null;
|
|
1179
|
+
if (path === null && !attrs.has('index'))
|
|
1180
|
+
return null;
|
|
1181
|
+
return { path: path ?? '', pkg };
|
|
1182
|
+
}
|
|
1183
|
+
/** Components a route renders: `element={<Page/>}`, `Component={Page}`, `component={Page}`. */
|
|
1184
|
+
routeComponents(ctx, valueOf) {
|
|
1185
|
+
const out = [];
|
|
1186
|
+
const element = valueOf('element');
|
|
1187
|
+
const inner = element?.type === 'jsx_expression' ? named(element)[0] ?? null : element;
|
|
1188
|
+
if (inner && (inner.type === 'jsx_self_closing_element' || inner.type === 'jsx_element')) {
|
|
1189
|
+
const n = this.jsxName(inner);
|
|
1190
|
+
if (n)
|
|
1191
|
+
out.push(...this.valueTargets(ctx, n));
|
|
1192
|
+
}
|
|
1193
|
+
for (const key of ['Component', 'component']) {
|
|
1194
|
+
const v = valueOf(key);
|
|
1195
|
+
const expr = v?.type === 'jsx_expression' ? named(v)[0] ?? null : v;
|
|
1196
|
+
if (expr)
|
|
1197
|
+
out.push(...this.valueTargets(ctx, expr));
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
/** `<Route>` elements, composed with every enclosing `<Route>`. */
|
|
1202
|
+
reactRouteElement(ctx, el) {
|
|
1203
|
+
const own = this.routeElementPath(ctx, el);
|
|
1204
|
+
if (!own)
|
|
1205
|
+
return;
|
|
1206
|
+
const segments = [own.path];
|
|
1207
|
+
for (let n = el.parent; n; n = n.parent) {
|
|
1208
|
+
if (n.type !== 'jsx_element')
|
|
1209
|
+
continue;
|
|
1210
|
+
const outer = this.routeElementPath(ctx, n);
|
|
1211
|
+
if (outer)
|
|
1212
|
+
segments.unshift(outer.path);
|
|
1213
|
+
}
|
|
1214
|
+
const attrs = this.jsxAttributes(el);
|
|
1215
|
+
const targets = this.routeComponents(ctx, k => attrs.get(k) ?? null);
|
|
1216
|
+
this.reactRoutes(ctx, el, segments, targets, own.pkg);
|
|
1217
|
+
}
|
|
1218
|
+
reactRoutes(ctx, at, segments, targets, pkg) {
|
|
1219
|
+
const path = composeRelative(segments);
|
|
1220
|
+
if (targets.length === 0) {
|
|
1221
|
+
this.unresolvedRegistrations.push({
|
|
1222
|
+
site: { file: ctx.path, line: at.startPosition.row + 1, col: at.startPosition.column },
|
|
1223
|
+
reason: 'handler-unresolved',
|
|
1224
|
+
evidence: `route ${path}: no component tied to a declaration`,
|
|
1225
|
+
});
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
for (const d of targets) {
|
|
1229
|
+
this.entry(d, 'route', 'declared', ctx.path, at.startPosition.row + 1, `${pkg} route`, { method: 'GET', path, framework: pkg, surface: 'page' });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
/** `createBrowserRouter([{ path, element, children }])` and the other factories. */
|
|
1233
|
+
reactRouterFactory(ctx, node) {
|
|
1234
|
+
const table = this.tables.reactRouter;
|
|
1235
|
+
const fn = node.childForFieldName('function');
|
|
1236
|
+
if (!table || fn?.type !== 'identifier' || !table.routerFactories.includes(fn.text))
|
|
1237
|
+
return;
|
|
1238
|
+
const pkg = this.reactRouterPackage(ctx, fn.text);
|
|
1239
|
+
const argsNode = node.childForFieldName('arguments');
|
|
1240
|
+
const list = argsNode ? named(argsNode)[0] : undefined;
|
|
1241
|
+
if (!pkg || list?.type !== 'array')
|
|
1242
|
+
return;
|
|
1243
|
+
const walk = (arr, parents) => {
|
|
1244
|
+
for (const obj of named(arr)) {
|
|
1245
|
+
if (obj.type !== 'object')
|
|
1246
|
+
continue;
|
|
1247
|
+
const fields = new Map();
|
|
1248
|
+
for (const pair of named(obj)) {
|
|
1249
|
+
if (pair.type === 'pair') {
|
|
1250
|
+
const key = pair.childForFieldName('key');
|
|
1251
|
+
const k = key?.type === 'string' ? stringValue(key) : key?.text;
|
|
1252
|
+
if (k)
|
|
1253
|
+
fields.set(k, pair.childForFieldName('value'));
|
|
1254
|
+
}
|
|
1255
|
+
else if (pair.type === 'shorthand_property_identifier')
|
|
1256
|
+
fields.set(pair.text, pair);
|
|
1257
|
+
}
|
|
1258
|
+
const p = fields.get('path');
|
|
1259
|
+
const own = p ? stringValue(p) : fields.has('index') ? '' : null;
|
|
1260
|
+
const segments = own === null ? parents : [...parents, own];
|
|
1261
|
+
const targets = this.routeComponents(ctx, k => fields.get(k) ?? null);
|
|
1262
|
+
if (own !== null)
|
|
1263
|
+
this.reactRoutes(ctx, obj, segments, targets, pkg);
|
|
1264
|
+
const children = fields.get('children');
|
|
1265
|
+
if (children?.type === 'array')
|
|
1266
|
+
walk(children, segments);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
walk(list, []);
|
|
1270
|
+
}
|
|
1271
|
+
// -- Next.js file routes -----------------------------------------------------
|
|
1272
|
+
/** Next.js `pages/`, `app/` and `middleware` conventions under `root`. */
|
|
1273
|
+
nextFileRoutes(root) {
|
|
1274
|
+
const prefix = normalizePath(root);
|
|
1275
|
+
for (const ctx of this.sortedFiles()) {
|
|
1276
|
+
if (prefix !== '' && !ctx.path.startsWith(prefix + '/'))
|
|
1277
|
+
continue;
|
|
1278
|
+
const rel = prefix === '' ? ctx.path : ctx.path.slice(prefix.length + 1);
|
|
1279
|
+
const m = rel.match(/^(?:src\/)?(pages|app)\/(.*)\.[cm]?[jt]sx?$/);
|
|
1280
|
+
const mw = /^(?:src\/)?middleware\.[cm]?[jt]s$/.test(rel);
|
|
1281
|
+
const exported = (name) => {
|
|
1282
|
+
const t = this.resolveExport(ctx, name, 0);
|
|
1283
|
+
return t.kind === 'decls' ? t.decls.filter(d => !this.bodiless.has(d.id)) : [];
|
|
1284
|
+
};
|
|
1285
|
+
const add = (d, kind, extra, evidence) => this.entry(d, kind, 'conventional', ctx.path, d.start_line, evidence, { framework: 'next', ...extra });
|
|
1286
|
+
if (mw) {
|
|
1287
|
+
for (const d of [...exported('middleware'), ...exported('default')])
|
|
1288
|
+
add(d, 'route', { method: 'USE', path: '/', surface: 'middleware' }, 'Next.js middleware');
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (!m)
|
|
1292
|
+
continue;
|
|
1293
|
+
const [, dir, rest] = m;
|
|
1294
|
+
const segments = rest.split('/');
|
|
1295
|
+
if (dir === 'pages') {
|
|
1296
|
+
const last = segments[segments.length - 1];
|
|
1297
|
+
if (/^_(app|document|error)$/.test(last)) {
|
|
1298
|
+
for (const d of exported('default'))
|
|
1299
|
+
add(d, 'lifecycle', {}, `Next.js ${last}`);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (last === 'index')
|
|
1303
|
+
segments.pop();
|
|
1304
|
+
const path = filePath(segments);
|
|
1305
|
+
const api = segments[0] === 'api';
|
|
1306
|
+
for (const d of exported('default'))
|
|
1307
|
+
add(d, 'route', { method: api ? 'ANY' : 'GET', path, surface: api ? 'api' : 'page' }, `Next.js pages/${rest}`);
|
|
1308
|
+
if (!api) {
|
|
1309
|
+
for (const n of ['getServerSideProps', 'getStaticProps', 'getStaticPaths', 'getInitialProps']) {
|
|
1310
|
+
for (const d of exported(n))
|
|
1311
|
+
add(d, 'lifecycle', { path }, `Next.js ${n}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const file = segments.pop() ?? '';
|
|
1317
|
+
const path = filePath(segments);
|
|
1318
|
+
if (file === 'page') {
|
|
1319
|
+
for (const d of exported('default'))
|
|
1320
|
+
add(d, 'route', { method: 'GET', path, surface: 'page' }, 'Next.js app page');
|
|
1321
|
+
}
|
|
1322
|
+
else if (file === 'layout' || file === 'template') {
|
|
1323
|
+
for (const d of exported('default'))
|
|
1324
|
+
add(d, 'route', { method: 'GET', path, surface: 'layout' }, `Next.js app ${file}`);
|
|
1325
|
+
}
|
|
1326
|
+
else if (file === 'route') {
|
|
1327
|
+
for (const method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) {
|
|
1328
|
+
for (const d of exported(method))
|
|
1329
|
+
add(d, 'route', { method, path, surface: 'api' }, 'Next.js route handler');
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
else
|
|
1333
|
+
continue;
|
|
1334
|
+
if (file === 'page' || file === 'layout') {
|
|
1335
|
+
for (const n of ['generateMetadata', 'generateStaticParams'])
|
|
1336
|
+
for (const d of exported(n))
|
|
1337
|
+
add(d, 'lifecycle', { path }, `Next.js ${n}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// -- React Native, Expo and Jest ---------------------------------------------
|
|
1342
|
+
/** Files a tool loads by name or place, for the tools the package depends on. */
|
|
1343
|
+
toolFileEntries(root, has) {
|
|
1344
|
+
const rows = (this.tables.toolFiles ?? []).filter(r => has(r.dependencies));
|
|
1345
|
+
if (rows.length === 0)
|
|
1346
|
+
return;
|
|
1347
|
+
const prefix = normalizePath(root);
|
|
1348
|
+
for (const ctx of this.sortedFiles()) {
|
|
1349
|
+
if (prefix !== '' && !ctx.path.startsWith(prefix + '/'))
|
|
1350
|
+
continue;
|
|
1351
|
+
const rel = prefix === '' ? ctx.path : ctx.path.slice(prefix.length + 1);
|
|
1352
|
+
if (rel.split('/').includes('node_modules'))
|
|
1353
|
+
continue;
|
|
1354
|
+
const row = rows.find(r => r.pattern.test(rel));
|
|
1355
|
+
if (row)
|
|
1356
|
+
this.toolFile(ctx, row.kind, row.tool, `loaded by ${row.tool} (${rel})`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/** The module and what it exports: the tool runs the file and uses its exports. */
|
|
1360
|
+
toolFile(ctx, kind, tool, evidence) {
|
|
1361
|
+
this.entry(ctx.module, kind, 'conventional', ctx.path, 1, evidence, { framework: tool });
|
|
1362
|
+
for (const d of this.exportedDecls(ctx))
|
|
1363
|
+
this.entry(d, kind, 'conventional', ctx.path, d.start_line, evidence, { framework: tool });
|
|
1364
|
+
}
|
|
1365
|
+
/** expo-router: every file under `app/` is a screen, `_layout` wraps them, `+` files are special. */
|
|
1366
|
+
expoFileRoutes(root) {
|
|
1367
|
+
const prefix = normalizePath(root);
|
|
1368
|
+
for (const ctx of this.sortedFiles()) {
|
|
1369
|
+
if (prefix !== '' && !ctx.path.startsWith(prefix + '/'))
|
|
1370
|
+
continue;
|
|
1371
|
+
const rel = prefix === '' ? ctx.path : ctx.path.slice(prefix.length + 1);
|
|
1372
|
+
const m = rel.match(/^(?:src\/)?app\/(.*)\.[cm]?[jt]sx?$/);
|
|
1373
|
+
if (!m || /\.d\.ts$/.test(rel))
|
|
1374
|
+
continue;
|
|
1375
|
+
const exported = (name) => {
|
|
1376
|
+
const t = this.resolveExport(ctx, name, 0);
|
|
1377
|
+
return t.kind === 'decls' ? t.decls.filter(d => !this.bodiless.has(d.id)) : [];
|
|
1378
|
+
};
|
|
1379
|
+
const add = (d, kind, extra, evidence) => this.entry(d, kind, 'conventional', ctx.path, d.start_line, evidence, { framework: 'expo-router', ...extra });
|
|
1380
|
+
const segments = m[1].split('/');
|
|
1381
|
+
const file = segments.pop() ?? '';
|
|
1382
|
+
const path = filePath(segments);
|
|
1383
|
+
const api = file.match(/^(.*)\+api$/);
|
|
1384
|
+
if (api) {
|
|
1385
|
+
const full = filePath([...segments, ...(api[1] === 'index' ? [] : [api[1]])]);
|
|
1386
|
+
for (const method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) {
|
|
1387
|
+
for (const d of exported(method))
|
|
1388
|
+
add(d, 'route', { method, path: full, surface: 'api' }, 'expo-router API route');
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
else if (file === '_layout') {
|
|
1392
|
+
for (const d of exported('default'))
|
|
1393
|
+
add(d, 'route', { method: 'GET', path, surface: 'layout' }, 'expo-router layout');
|
|
1394
|
+
for (const d of exported('ErrorBoundary'))
|
|
1395
|
+
add(d, 'lifecycle', { path }, 'expo-router ErrorBoundary');
|
|
1396
|
+
}
|
|
1397
|
+
else if (file === '+html' || file === '+native-intent') {
|
|
1398
|
+
for (const d of exported('default'))
|
|
1399
|
+
add(d, 'lifecycle', { path }, `expo-router ${file}`);
|
|
1400
|
+
for (const d of exported('redirectSystemPath'))
|
|
1401
|
+
add(d, 'lifecycle', { path }, `expo-router ${file}`);
|
|
1402
|
+
}
|
|
1403
|
+
else if (file.startsWith('+')) {
|
|
1404
|
+
for (const d of exported('default'))
|
|
1405
|
+
add(d, 'route', { method: 'GET', path, surface: 'page' }, `expo-router ${file}`);
|
|
1406
|
+
}
|
|
1407
|
+
else if (!file.startsWith('_')) {
|
|
1408
|
+
const full = filePath(file === 'index' ? segments : [...segments, file]);
|
|
1409
|
+
for (const d of exported('default'))
|
|
1410
|
+
add(d, 'route', { method: 'GET', path: full, surface: 'page' }, 'expo-router screen');
|
|
1411
|
+
for (const n of ['ErrorBoundary', 'generateStaticParams'])
|
|
1412
|
+
for (const d of exported(n))
|
|
1413
|
+
add(d, 'lifecycle', { path: full }, `expo-router ${n}`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/** Config plugins named by a relative path in `app.json` or `app.config.*`. */
|
|
1418
|
+
expoPlugins(pkg, where) {
|
|
1419
|
+
const specs = [];
|
|
1420
|
+
if (pkg.appJson !== undefined) {
|
|
1421
|
+
try {
|
|
1422
|
+
const j = (JSON.parse(pkg.appJson) ?? {});
|
|
1423
|
+
const expo = (j.expo ?? j);
|
|
1424
|
+
for (const p of Array.isArray(expo.plugins) ? expo.plugins : []) {
|
|
1425
|
+
const s = Array.isArray(p) ? p[0] : p;
|
|
1426
|
+
if (typeof s === 'string')
|
|
1427
|
+
specs.push(s);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
catch {
|
|
1431
|
+
this.unresolvedRegistrations.push({ site: { ...where, file: normalizePath(`${pkg.root}/app.json`) }, reason: 'manifest-unresolved', evidence: 'app.json does not parse' });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const config = this.findFile(normalizePath(`${pkg.root}/app.config.ts`));
|
|
1435
|
+
const ctx = config ? this.files.get(config) : undefined;
|
|
1436
|
+
if (ctx) {
|
|
1437
|
+
const visit = (n) => {
|
|
1438
|
+
if (n.type === 'pair') {
|
|
1439
|
+
const key = n.childForFieldName('key');
|
|
1440
|
+
const value = n.childForFieldName('value');
|
|
1441
|
+
const k = key?.type === 'string' ? stringValue(key) : key?.text;
|
|
1442
|
+
if (k === 'plugins' && value?.type === 'array') {
|
|
1443
|
+
for (const el of named(value)) {
|
|
1444
|
+
const first = el.type === 'array' ? named(el)[0] : el;
|
|
1445
|
+
const s = first && first.type === 'string' ? stringValue(first) : null;
|
|
1446
|
+
if (s)
|
|
1447
|
+
specs.push(s);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
for (const c of named(n))
|
|
1452
|
+
visit(c);
|
|
1453
|
+
};
|
|
1454
|
+
visit(ctx.root);
|
|
1455
|
+
}
|
|
1456
|
+
for (const spec of [...new Set(specs)].filter(s => s.startsWith('.'))) {
|
|
1457
|
+
const file = this.sourceFor(pkg.root, spec);
|
|
1458
|
+
const plugin = file ? this.files.get(file) : undefined;
|
|
1459
|
+
if (plugin)
|
|
1460
|
+
this.toolFile(plugin, 'main', 'expo', `Expo config plugin (${spec})`);
|
|
1461
|
+
else
|
|
1462
|
+
this.unresolvedRegistrations.push({ site: where, reason: 'manifest-unresolved', evidence: `config plugin ${spec} not found in the tree` });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Where a router receiver comes from: a framework package (declared) or
|
|
1467
|
+
* unknown (inferred). `key` names the router object across files.
|
|
1468
|
+
*/
|
|
1469
|
+
routerOrigin(ctx, receiver, depth) {
|
|
1470
|
+
if (receiver?.type !== 'identifier' || depth > 3)
|
|
1471
|
+
return { certainty: 'inferred' };
|
|
1472
|
+
const local = this.localBinding(receiver, receiver.text);
|
|
1473
|
+
const declarator = local?.kind === 'var' ? local.node : local ? null : ctx.topVars.get(receiver.text) ?? null;
|
|
1474
|
+
if (!declarator) {
|
|
1475
|
+
const imp = ctx.imports.get(receiver.text);
|
|
1476
|
+
if ((imp?.kind === 'named' || imp?.kind === 'default') && imp.module) {
|
|
1477
|
+
const target = this.files.get(imp.module);
|
|
1478
|
+
const name = imp.kind === 'named' ? imp.name : target?.defaultLocal;
|
|
1479
|
+
const v = target && name ? target.topVars.get(name) : undefined;
|
|
1480
|
+
if (target && v)
|
|
1481
|
+
return this.routerOrigin(target, v.childForFieldName('name'), depth + 1);
|
|
1482
|
+
}
|
|
1483
|
+
return { certainty: 'inferred' };
|
|
1484
|
+
}
|
|
1485
|
+
const value = declarator.childForFieldName('value');
|
|
1486
|
+
if (value?.type !== 'call_expression' && value?.type !== 'new_expression')
|
|
1487
|
+
return { certainty: 'inferred' };
|
|
1488
|
+
let callee = value.type === 'call_expression' ? value.childForFieldName('function') : value.childForFieldName('constructor');
|
|
1489
|
+
while (callee?.type === 'member_expression')
|
|
1490
|
+
callee = callee.childForFieldName('object');
|
|
1491
|
+
if (callee?.type !== 'identifier')
|
|
1492
|
+
return { certainty: 'inferred' };
|
|
1493
|
+
const imp = ctx.imports.get(callee.text);
|
|
1494
|
+
if (imp && imp.module === null && this.tables.routes.packages.includes(imp.specifier)) {
|
|
1495
|
+
const key = `${ctx.path}#${declarator.startPosition.row + 1}:${declarator.startPosition.column}`;
|
|
1496
|
+
return { certainty: 'declared', framework: imp.specifier, key };
|
|
1497
|
+
}
|
|
1498
|
+
return { certainty: 'inferred' };
|
|
1499
|
+
}
|
|
1500
|
+
decoratorsOf(node) {
|
|
1501
|
+
const own = named(node).filter(c => c.type === 'decorator');
|
|
1502
|
+
const parent = node.parent?.type === 'export_statement' ? named(node.parent).filter(c => c.type === 'decorator') : [];
|
|
1503
|
+
return [...parent, ...own];
|
|
1504
|
+
}
|
|
1505
|
+
decoratorCall(ctx, dec) {
|
|
1506
|
+
const inner = named(dec)[0];
|
|
1507
|
+
let callee = inner ?? null;
|
|
1508
|
+
let args = [];
|
|
1509
|
+
if (inner?.type === 'call_expression') {
|
|
1510
|
+
callee = inner.childForFieldName('function');
|
|
1511
|
+
const a = inner.childForFieldName('arguments');
|
|
1512
|
+
args = a ? named(a) : [];
|
|
1513
|
+
}
|
|
1514
|
+
if (callee?.type !== 'identifier')
|
|
1515
|
+
return null;
|
|
1516
|
+
const imp = ctx.imports.get(callee.text);
|
|
1517
|
+
if (!imp)
|
|
1518
|
+
return null;
|
|
1519
|
+
return { name: imp.kind === 'named' ? imp.name : callee.text, pkg: imp.specifier, args };
|
|
1520
|
+
}
|
|
1521
|
+
decoratorEntries(ctx, classNode, cls) {
|
|
1522
|
+
const classDecs = this.decoratorsOf(classNode).map(d => this.decoratorCall(ctx, d)).filter(x => x !== null);
|
|
1523
|
+
const rows = this.tables.decorators;
|
|
1524
|
+
for (const row of rows) {
|
|
1525
|
+
if (row.on !== 'class')
|
|
1526
|
+
continue;
|
|
1527
|
+
const hit = classDecs.find(d => d.pkg === row.package && row.names.includes(d.name));
|
|
1528
|
+
if (!hit)
|
|
1529
|
+
continue;
|
|
1530
|
+
const ctor = this.membersOf(cls, 'constructor').find(d => !this.bodiless.has(d.id));
|
|
1531
|
+
this.entry(ctor ?? cls, row.kind, 'declared', ctx.path, cls.start_line, `@${hit.name} from ${row.package}`, { framework: row.package });
|
|
1532
|
+
}
|
|
1533
|
+
const body = classNode.childForFieldName('body');
|
|
1534
|
+
if (!body)
|
|
1535
|
+
return;
|
|
1536
|
+
let pending = [];
|
|
1537
|
+
for (const m of named(body)) {
|
|
1538
|
+
if (m.type === 'decorator') {
|
|
1539
|
+
pending.push(m);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (m.type === 'method_definition' && pending.length > 0) {
|
|
1543
|
+
const decl = ctx.byStart.get(posKey(m, 'method'));
|
|
1544
|
+
for (const dec of pending) {
|
|
1545
|
+
const info = this.decoratorCall(ctx, dec);
|
|
1546
|
+
const row = info ? rows.find(r => r.on === 'method' && r.package === info.pkg && r.names.includes(info.name)) : undefined;
|
|
1547
|
+
if (!info || !row || !decl)
|
|
1548
|
+
continue;
|
|
1549
|
+
const extra = { framework: row.package };
|
|
1550
|
+
if (row.kind === 'route') {
|
|
1551
|
+
const prefixDec = row.prefixFrom ? classDecs.find(d => d.pkg === row.package && d.name === row.prefixFrom) : undefined;
|
|
1552
|
+
const prefix = stringValue(prefixDec?.args[0] ?? null) ?? '';
|
|
1553
|
+
const own = stringValue(info.args[0] ?? null) ?? '';
|
|
1554
|
+
extra.method = info.name.toUpperCase();
|
|
1555
|
+
extra.path = '/' + [prefix, own].map(x => x.replace(/^\/+|\/+$/g, '')).filter(x => x !== '').join('/');
|
|
1556
|
+
}
|
|
1557
|
+
this.entry(decl, row.kind, 'declared', ctx.path, decl.start_line, `@${info.name} from ${row.package}`, extra);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
pending = [];
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
findFile(stem) {
|
|
1564
|
+
if (this.files.has(stem))
|
|
1565
|
+
return stem;
|
|
1566
|
+
const bare = stem.replace(/\.(?:d\.ts|[mc]?[jt]sx?)$/, '');
|
|
1567
|
+
for (const ext of EXTENSIONS)
|
|
1568
|
+
if (this.files.has(bare + ext))
|
|
1569
|
+
return bare + ext;
|
|
1570
|
+
for (const ext of EXTENSIONS)
|
|
1571
|
+
if (this.files.has(`${bare}/index${ext}`))
|
|
1572
|
+
return `${bare}/index${ext}`;
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
/** A manifest path to a source file: as written, else the build output mapped back to `src/`. */
|
|
1576
|
+
sourceFor(root, p) {
|
|
1577
|
+
const base = normalizePath(`${root}/${p}`);
|
|
1578
|
+
const direct = this.findFile(base);
|
|
1579
|
+
if (direct)
|
|
1580
|
+
return direct;
|
|
1581
|
+
const prefix = normalizePath(root);
|
|
1582
|
+
const inner = prefix === '' ? base : base.slice(prefix.length + 1);
|
|
1583
|
+
const mapped = inner.replace(/^(?:dist|lib|build|out)(?:\/(?:esm|cjs|src))?\//, 'src/');
|
|
1584
|
+
return mapped === inner ? null : this.findFile(normalizePath(`${prefix}/${mapped}`));
|
|
1585
|
+
}
|
|
1586
|
+
/** Names a file exports, re-exports included; every module-level name for a file without ES exports. */
|
|
1587
|
+
exportedDecls(ctx, seen = new Set()) {
|
|
1588
|
+
if (seen.has(ctx.path))
|
|
1589
|
+
return [];
|
|
1590
|
+
seen.add(ctx.path);
|
|
1591
|
+
const names = new Set([...ctx.exports.keys(), ...ctx.reexports.keys()]);
|
|
1592
|
+
if (ctx.defaultLocal !== null || ctx.top.has('default'))
|
|
1593
|
+
names.add('default');
|
|
1594
|
+
if (!ctx.esm)
|
|
1595
|
+
for (const n of ctx.top.keys())
|
|
1596
|
+
names.add(n);
|
|
1597
|
+
const out = [];
|
|
1598
|
+
for (const n of [...names].sort()) {
|
|
1599
|
+
const t = this.resolveExport(ctx, n, 0);
|
|
1600
|
+
if (t.kind === 'decls')
|
|
1601
|
+
out.push(...t.decls.filter(d => !this.bodiless.has(d.id)));
|
|
1602
|
+
}
|
|
1603
|
+
for (const m of ctx.stars) {
|
|
1604
|
+
const target = m === null ? undefined : this.files.get(m);
|
|
1605
|
+
if (target)
|
|
1606
|
+
out.push(...this.exportedDecls(target, seen));
|
|
1607
|
+
}
|
|
1608
|
+
return out;
|
|
1609
|
+
}
|
|
1610
|
+
exportEntries(ctx, evidence) {
|
|
1611
|
+
for (const d of this.exportedDecls(ctx))
|
|
1612
|
+
this.entry(d, 'export', 'declared', ctx.path, d.start_line, evidence);
|
|
1613
|
+
}
|
|
1614
|
+
/** Entries from package manifests and the caller's lists. */
|
|
1615
|
+
manifestEntries() {
|
|
1616
|
+
for (const pkg of this.options.packages ?? []) {
|
|
1617
|
+
const where = { file: normalizePath(`${pkg.root}/package.json`), line: 1, col: 0 };
|
|
1618
|
+
let json;
|
|
1619
|
+
try {
|
|
1620
|
+
json = JSON.parse(pkg.packageJson);
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
this.unresolvedRegistrations.push({ site: where, reason: 'manifest-unresolved', evidence: 'package.json does not parse' });
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const m = (json ?? {});
|
|
1627
|
+
const strings = (v, out = []) => {
|
|
1628
|
+
if (typeof v === 'string')
|
|
1629
|
+
out.push(v);
|
|
1630
|
+
else if (Array.isArray(v))
|
|
1631
|
+
for (const x of v)
|
|
1632
|
+
strings(x, out);
|
|
1633
|
+
else if (v && typeof v === 'object')
|
|
1634
|
+
for (const k of Object.keys(v).sort())
|
|
1635
|
+
strings(v[k], out);
|
|
1636
|
+
return out;
|
|
1637
|
+
};
|
|
1638
|
+
const exportPaths = [...strings(m.main), ...strings(m.module), ...strings(m.exports)].filter(p => !p.endsWith('.json') && !p.endsWith('.d.ts') && !p.includes('*'));
|
|
1639
|
+
for (const p of [...new Set(exportPaths)]) {
|
|
1640
|
+
const file = this.sourceFor(pkg.root, p);
|
|
1641
|
+
const ctx = file ? this.files.get(file) : undefined;
|
|
1642
|
+
if (ctx)
|
|
1643
|
+
this.exportEntries(ctx, `exported by package.json (${p})`);
|
|
1644
|
+
else
|
|
1645
|
+
this.unresolvedRegistrations.push({ site: where, reason: 'manifest-unresolved', evidence: `export path ${p} not found in the tree` });
|
|
1646
|
+
}
|
|
1647
|
+
const deps = { ...m.dependencies, ...m.devDependencies };
|
|
1648
|
+
const next = this.tables.nextFileRoutes;
|
|
1649
|
+
if (next && Object.prototype.hasOwnProperty.call(deps, next.dependency))
|
|
1650
|
+
this.nextFileRoutes(pkg.root);
|
|
1651
|
+
this.scriptEntries(pkg.root, m.scripts);
|
|
1652
|
+
const has = (names) => names.some(n => Object.prototype.hasOwnProperty.call(deps, n));
|
|
1653
|
+
this.toolFileEntries(pkg.root, has);
|
|
1654
|
+
if (this.tables.expo && has(this.tables.expo.dependencies)) {
|
|
1655
|
+
this.expoFileRoutes(pkg.root);
|
|
1656
|
+
this.expoPlugins(pkg, where);
|
|
1657
|
+
}
|
|
1658
|
+
for (const p of [...new Set(strings(m.bin))]) {
|
|
1659
|
+
const file = this.sourceFor(pkg.root, p);
|
|
1660
|
+
const ctx = file ? this.files.get(file) : undefined;
|
|
1661
|
+
if (ctx)
|
|
1662
|
+
this.entry(ctx.module, 'main', 'declared', ctx.path, 1, `package.json bin (${p})`);
|
|
1663
|
+
else
|
|
1664
|
+
this.unresolvedRegistrations.push({ site: where, reason: 'manifest-unresolved', evidence: `bin ${p} not found in the tree` });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (this.tables.nextFileRoutes)
|
|
1668
|
+
for (const root of this.options.fileRouteRoots?.next ?? [])
|
|
1669
|
+
this.nextFileRoutes(root);
|
|
1670
|
+
this.composeRoutes();
|
|
1671
|
+
for (const f of this.options.exportRoots ?? []) {
|
|
1672
|
+
const ctx = this.files.get(normalizePath(f));
|
|
1673
|
+
if (ctx)
|
|
1674
|
+
this.exportEntries(ctx, 'export root given by the caller');
|
|
1675
|
+
}
|
|
1676
|
+
for (const f of this.options.mainFiles ?? []) {
|
|
1677
|
+
const ctx = this.files.get(normalizePath(f));
|
|
1678
|
+
if (ctx)
|
|
1679
|
+
this.entry(ctx.module, 'main', 'declared', ctx.path, 1, 'program file given by the caller');
|
|
1680
|
+
}
|
|
1681
|
+
for (const ctx of this.sortedFiles()) {
|
|
1682
|
+
if (this.tables.tests.filePattern.test(ctx.path))
|
|
1683
|
+
this.entry(ctx.module, 'test', 'conventional', ctx.path, 1, 'test file loaded by the runner');
|
|
1684
|
+
}
|
|
1685
|
+
this.programFiles();
|
|
1686
|
+
}
|
|
1687
|
+
/** Files a `package.json` script runs (`node scripts/build.mjs`, `tsx bench/x.ts`): `main` entries. */
|
|
1688
|
+
scriptEntries(root, scripts) {
|
|
1689
|
+
const table = this.tables.scripts;
|
|
1690
|
+
if (!table || !scripts || typeof scripts !== 'object')
|
|
1691
|
+
return;
|
|
1692
|
+
for (const name of Object.keys(scripts).sort()) {
|
|
1693
|
+
const cmd = scripts[name];
|
|
1694
|
+
if (typeof cmd !== 'string')
|
|
1695
|
+
continue;
|
|
1696
|
+
for (const raw of cmd.split(/\s+|&&|\|\||[;|()]/)) {
|
|
1697
|
+
const token = raw.replace(/^['"]|['"]$/g, '').replace(/^\.\//, '');
|
|
1698
|
+
if (!/\.[cm]?[jt]sx?$/.test(token) || token.startsWith('-') || token.includes('*'))
|
|
1699
|
+
continue;
|
|
1700
|
+
const file = this.findFile(normalizePath(`${root}/${token}`));
|
|
1701
|
+
const ctx = file ? this.files.get(file) : undefined;
|
|
1702
|
+
if (ctx)
|
|
1703
|
+
this.entry(ctx.module, 'main', 'declared', ctx.path, 1, `package.json script ${name}`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/** Files in script directories, and files starting with `#!`: run as programs (`main`, conventional). */
|
|
1708
|
+
programFiles() {
|
|
1709
|
+
const table = this.tables.scripts;
|
|
1710
|
+
if (!table)
|
|
1711
|
+
return;
|
|
1712
|
+
const roots = [...new Set((this.options.packages ?? []).map(p => normalizePath(p.root)))];
|
|
1713
|
+
if (roots.length === 0)
|
|
1714
|
+
roots.push('');
|
|
1715
|
+
for (const ctx of this.sortedFiles()) {
|
|
1716
|
+
const inDir = roots.some(r => table.dirs.some(d => ctx.path.startsWith(normalizePath(`${r}/${d}`) + '/')));
|
|
1717
|
+
const shebang = table.shebang && ctx.root.firstChild?.type === 'hash_bang_line';
|
|
1718
|
+
if (inDir)
|
|
1719
|
+
this.entry(ctx.module, 'main', 'conventional', ctx.path, 1, 'file in a script directory');
|
|
1720
|
+
else if (shebang)
|
|
1721
|
+
this.entry(ctx.module, 'main', 'conventional', ctx.path, 1, 'file starts with #!');
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
importEdges() {
|
|
1725
|
+
const out = new Map();
|
|
1726
|
+
for (const ctx of this.sortedFiles()) {
|
|
1727
|
+
for (const m of ctx.loads) {
|
|
1728
|
+
const to = this.files.get(m);
|
|
1729
|
+
if (to && to !== ctx)
|
|
1730
|
+
out.set(`${ctx.module.id}>${to.module.id}`, { from: ctx.module.id, to: to.module.id });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return [...out.values()];
|
|
1734
|
+
}
|
|
1735
|
+
site(ctx, node) {
|
|
1736
|
+
const fn = node.type === 'new_expression' ? node.childForFieldName('constructor') : node.childForFieldName('function');
|
|
1737
|
+
if (!fn)
|
|
1738
|
+
return;
|
|
1739
|
+
const site = { file: ctx.path, line: node.startPosition.row + 1, col: node.startPosition.column };
|
|
1740
|
+
// Module loading is not a call: a literal specifier is a load edge, a computed one an unresolved site.
|
|
1741
|
+
const load = node.type === 'call_expression' && (fn.type === 'import' || !this.localBinding(fn, 'require')) ? moduleLoad(node) : null;
|
|
1742
|
+
if (load) {
|
|
1743
|
+
const owner = ctx.locate(site.line, site.col) ?? ctx.module;
|
|
1744
|
+
if (load.spec !== null) {
|
|
1745
|
+
const module = this.resolveModule(ctx.path, load.spec);
|
|
1746
|
+
const to = module ? this.files.get(module) : undefined;
|
|
1747
|
+
if (!module || !to)
|
|
1748
|
+
return;
|
|
1749
|
+
if (owner === ctx.module)
|
|
1750
|
+
ctx.loads.push(module);
|
|
1751
|
+
else
|
|
1752
|
+
this.loadEdges.push({ from: owner.id, to: to.module.id, site });
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
this.sites++;
|
|
1756
|
+
this.unresolved.push({ from: owner.id, methodName: COMPUTED_MODULE_LOAD, site, reason: 'dynamic', evidence: 'module loaded by a computed specifier' });
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
this.sites++;
|
|
1760
|
+
const from = (ctx.locate(site.line, site.col) ?? ctx.module).id;
|
|
1761
|
+
const edge = (to, tier, methodName, evidence, via) => {
|
|
1762
|
+
const e = { from, to: to.id, tier, methodName, site, evidence };
|
|
1763
|
+
if (via)
|
|
1764
|
+
e.via = via.id;
|
|
1765
|
+
this.edges.push(e);
|
|
1766
|
+
};
|
|
1767
|
+
const unresolved = (methodName, reason, evidence) => {
|
|
1768
|
+
this.unresolved.push({ from, methodName, site, reason, evidence });
|
|
1769
|
+
};
|
|
1770
|
+
if (node.type === 'new_expression') {
|
|
1771
|
+
this.construct(ctx, fn, edge, unresolved);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (fn.type === 'super') {
|
|
1775
|
+
const cls = this.enclosingClass(ctx, node);
|
|
1776
|
+
const sup = cls ? this.supers.get(cls.id)?.extends : null;
|
|
1777
|
+
if (sup === 'external')
|
|
1778
|
+
unresolved('super', 'external', 'superclass outside the tree');
|
|
1779
|
+
else if (sup)
|
|
1780
|
+
this.constructType(sup, 'super', edge);
|
|
1781
|
+
else
|
|
1782
|
+
unresolved('super', 'unknown', 'no superclass found');
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (fn.type === 'identifier') {
|
|
1786
|
+
this.callName(ctx, fn, edge, unresolved);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (fn.type === 'parenthesized_expression') {
|
|
1790
|
+
const inner = named(fn)[0];
|
|
1791
|
+
const d = inner && FUNCTION_NODES.has(inner.type) ? this.nestedDeclAt(ctx, inner) : null;
|
|
1792
|
+
if (d)
|
|
1793
|
+
edge(d, 'exact', '<iife>', 'immediately invoked function');
|
|
1794
|
+
else
|
|
1795
|
+
unresolved('<expr>', 'dynamic', 'callee is an expression');
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (fn.type === 'member_expression') {
|
|
1799
|
+
this.callMember(ctx, fn, edge, unresolved);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
unresolved('<expr>', 'dynamic', `callee is a ${fn.type}`);
|
|
1803
|
+
}
|
|
1804
|
+
construct(ctx, fn, edge, unresolved) {
|
|
1805
|
+
const name = fn.text;
|
|
1806
|
+
if (fn.type !== 'identifier' && fn.type !== 'member_expression') {
|
|
1807
|
+
unresolved(name, 'dynamic', 'constructed from an expression');
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (fn.type === 'identifier' && this.localBinding(fn, fn.text)) {
|
|
1811
|
+
unresolved(name, 'dynamic', 'class bound to a local value');
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const t = this.resolveTypeRef(ctx, fn);
|
|
1815
|
+
if (t === 'external' || t === null)
|
|
1816
|
+
unresolved(name, 'external', 'class outside the tree');
|
|
1817
|
+
else
|
|
1818
|
+
this.constructType(t, name, edge);
|
|
1819
|
+
}
|
|
1820
|
+
constructType(t, name, edge) {
|
|
1821
|
+
const ctor = this.membersOf(t, 'constructor').find(d => !this.bodiless.has(d.id));
|
|
1822
|
+
edge(ctor ?? t, 'exact', name, ctor ? 'constructor of a resolved class' : 'class without its own constructor');
|
|
1823
|
+
}
|
|
1824
|
+
callName(ctx, fn, edge, unresolved) {
|
|
1825
|
+
const name = fn.text;
|
|
1826
|
+
const local = this.localBinding(fn, name);
|
|
1827
|
+
if (local) {
|
|
1828
|
+
const loaded = local.kind === 'var' ? this.loadBinding(ctx, local.node, name) : null;
|
|
1829
|
+
if (loaded) {
|
|
1830
|
+
this.bindTarget(this.resolveBinding(loaded), name, edge, unresolved, 'bound from a module load');
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const holder = local.kind === 'var' ? local.node.childForFieldName('value') : local.kind === 'fn' ? local.node : null;
|
|
1834
|
+
const d = holder && FUNCTION_NODES.has(holder.type) ? this.nestedDeclAt(ctx, holder) : null;
|
|
1835
|
+
if (d)
|
|
1836
|
+
edge(d, 'exact', name, 'nested function in lexical scope');
|
|
1837
|
+
else
|
|
1838
|
+
unresolved(name, 'dynamic', local.kind === 'param' ? 'callee is a parameter' : 'callee is a local value');
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
this.bindTarget(this.resolveLocal(ctx, name), name, edge, unresolved, 'module scope or import');
|
|
1842
|
+
}
|
|
1843
|
+
bindTarget(target, name, edge, unresolved, how) {
|
|
1844
|
+
if (target.kind === 'external') {
|
|
1845
|
+
unresolved(name, 'external', 'imported from outside the tree');
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (target.kind === 'none') {
|
|
1849
|
+
unresolved(name, 'external', 'not declared in the tree (global or ambient)');
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (target.kind === 'namespace') {
|
|
1853
|
+
unresolved(name, 'dynamic', 'a module namespace is not callable');
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const fns = target.decls.filter(d => d.kind === 'function');
|
|
1857
|
+
if (fns.length === 0) {
|
|
1858
|
+
const type = target.decls.find(d => d.kind === 'type');
|
|
1859
|
+
if (type)
|
|
1860
|
+
unresolved(name, 'unknown', 'a class called without new');
|
|
1861
|
+
else
|
|
1862
|
+
unresolved(name, 'dynamic', 'callee is a value, not a declared function');
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const bodied = fns.filter(d => !this.bodiless.has(d.id));
|
|
1866
|
+
if (bodied.length === 1)
|
|
1867
|
+
edge(bodied[0], 'exact', name, how);
|
|
1868
|
+
else if (bodied.length === 0)
|
|
1869
|
+
unresolved(name, 'external', 'declared without a body (ambient)');
|
|
1870
|
+
else
|
|
1871
|
+
unresolved(name, 'overload-ambiguous', `${bodied.length} bodies with this name`);
|
|
1872
|
+
}
|
|
1873
|
+
/** The namespace a module load gives: a local bound to one, or the load expression itself. */
|
|
1874
|
+
loadedNamespace(ctx, object) {
|
|
1875
|
+
let binding = null;
|
|
1876
|
+
if (object.type === 'identifier') {
|
|
1877
|
+
const local = this.localBinding(object, object.text);
|
|
1878
|
+
binding = local?.kind === 'var' ? this.loadBinding(ctx, local.node, object.text) : null;
|
|
1879
|
+
}
|
|
1880
|
+
else {
|
|
1881
|
+
const load = moduleLoad(object);
|
|
1882
|
+
if (load?.spec != null)
|
|
1883
|
+
binding = { kind: 'namespace', module: this.resolveModule(ctx.path, load.spec), specifier: load.spec };
|
|
1884
|
+
}
|
|
1885
|
+
if (binding?.kind !== 'namespace')
|
|
1886
|
+
return null;
|
|
1887
|
+
const t = this.resolveBinding(binding);
|
|
1888
|
+
return t.kind === 'namespace' || t.kind === 'external' ? t : null;
|
|
1889
|
+
}
|
|
1890
|
+
callMember(ctx, fn, edge, unresolved) {
|
|
1891
|
+
const object = fn.childForFieldName('object');
|
|
1892
|
+
const property = fn.childForFieldName('property');
|
|
1893
|
+
if (!object || !property) {
|
|
1894
|
+
unresolved(fn.text, 'dynamic', 'incomplete member expression');
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const name = property.text;
|
|
1898
|
+
if (property.type === 'private_property_identifier' && object.type !== 'this') {
|
|
1899
|
+
unresolved(name, 'unknown', 'private member on another receiver');
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
// `super.m()`
|
|
1903
|
+
if (object.type === 'super') {
|
|
1904
|
+
const cls = this.enclosingClass(ctx, fn);
|
|
1905
|
+
const sup = cls ? this.supers.get(cls.id)?.extends : null;
|
|
1906
|
+
if (!sup || sup === 'external') {
|
|
1907
|
+
unresolved(name, sup === 'external' ? 'external' : 'unknown', 'superclass method');
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const found = this.findMember(sup, name);
|
|
1911
|
+
if (found === 'external' || !found)
|
|
1912
|
+
unresolved(name, found === 'external' ? 'external' : 'unknown', 'superclass method not in the tree');
|
|
1913
|
+
else
|
|
1914
|
+
this.bindMembers(found.decls, name, 'exact', edge, unresolved, 'super call');
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
// `m.f()` on `const m = await import('./m')`, `(await import('./m')).f()`
|
|
1918
|
+
const loadedNs = this.loadedNamespace(ctx, object);
|
|
1919
|
+
if (loadedNs) {
|
|
1920
|
+
if (loadedNs.kind === 'namespace') {
|
|
1921
|
+
const member = this.resolveExport(loadedNs.ctx, name, 0);
|
|
1922
|
+
if (member.kind === 'none')
|
|
1923
|
+
unresolved(name, 'unknown', 'not exported by a module in the tree');
|
|
1924
|
+
else
|
|
1925
|
+
this.bindTarget(member, name, edge, unresolved, 'module namespace from a module load');
|
|
1926
|
+
}
|
|
1927
|
+
else
|
|
1928
|
+
unresolved(name, 'external', 'module loaded from outside the tree');
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
// `ns.f()` on a module namespace, `C.m()` on a class name
|
|
1932
|
+
if (object.type === 'identifier' && !this.localBinding(object, object.text)) {
|
|
1933
|
+
const target = this.resolveLocal(ctx, object.text);
|
|
1934
|
+
if (target.kind === 'namespace') {
|
|
1935
|
+
const member = this.resolveExport(target.ctx, name, 0);
|
|
1936
|
+
if (member.kind === 'none')
|
|
1937
|
+
unresolved(name, 'unknown', 'not exported by a module in the tree');
|
|
1938
|
+
else
|
|
1939
|
+
this.bindTarget(member, name, edge, unresolved, 'module namespace');
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (target.kind === 'external' || (target.kind === 'none' && !ctx.topVars.has(object.text))) {
|
|
1943
|
+
unresolved(name, 'external', 'receiver declared outside the tree');
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (target.kind === 'decls') {
|
|
1947
|
+
const type = target.decls.find(d => d.kind === 'type');
|
|
1948
|
+
if (type) {
|
|
1949
|
+
const found = this.findMember(type, name);
|
|
1950
|
+
if (found && found !== 'external')
|
|
1951
|
+
this.bindMembers(found.decls, name, 'exact', edge, unresolved, 'static member of a resolved class');
|
|
1952
|
+
else
|
|
1953
|
+
unresolved(name, found === 'external' ? 'external' : 'unknown', 'static member not found');
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const type = this.typeOf(ctx, object);
|
|
1959
|
+
if (type === 'external') {
|
|
1960
|
+
unresolved(name, 'external', 'receiver type declared outside the tree');
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (type) {
|
|
1964
|
+
this.dispatch(type, name, edge, unresolved);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
this.infer(name, edge, unresolved);
|
|
1968
|
+
}
|
|
1969
|
+
dispatch(type, name, edge, unresolved) {
|
|
1970
|
+
const found = this.findMember(type, name);
|
|
1971
|
+
if (found === 'external') {
|
|
1972
|
+
unresolved(name, 'external', 'member inherited from outside the tree');
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (!found) {
|
|
1976
|
+
unresolved(name, 'unknown', 'member not found on the receiver type');
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const bodied = found.decls.filter(d => !this.bodiless.has(d.id));
|
|
1980
|
+
if (bodied.length > 1) {
|
|
1981
|
+
unresolved(name, 'overload-ambiguous', `${bodied.length} bodies with this name`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const declaring = bodied[0] ?? found.decls[0];
|
|
1985
|
+
const subs = this.overrides(found.owner, name);
|
|
1986
|
+
if (bodied.length === 1 && subs.length === 0) {
|
|
1987
|
+
edge(bodied[0], 'exact', name, 'receiver type known; no override in the tree');
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const bodies = [...(bodied.length === 1 ? [bodied[0]] : []), ...subs];
|
|
1991
|
+
if (bodies.length === 0) {
|
|
1992
|
+
unresolved(name, 'external', 'abstract or interface member with no implementation in the tree');
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
for (const b of bodies)
|
|
1996
|
+
edge(b, 'polymorphic', name, 'receiver type known; dispatch open', declaring);
|
|
1997
|
+
}
|
|
1998
|
+
bindMembers(decls, name, tier, edge, unresolved, evidence) {
|
|
1999
|
+
const bodied = decls.filter(d => !this.bodiless.has(d.id));
|
|
2000
|
+
if (bodied.length === 1)
|
|
2001
|
+
edge(bodied[0], tier, name, evidence);
|
|
2002
|
+
else if (bodied.length === 0)
|
|
2003
|
+
unresolved(name, 'unknown', 'member declared without a body');
|
|
2004
|
+
else
|
|
2005
|
+
unresolved(name, 'overload-ambiguous', `${bodied.length} bodies with this name`);
|
|
2006
|
+
}
|
|
2007
|
+
infer(name, edge, unresolved) {
|
|
2008
|
+
const all = (this.methodsByName.get(name) ?? []).filter(d => !this.bodiless.has(d.id));
|
|
2009
|
+
if (all.length === 0) {
|
|
2010
|
+
unresolved(name, 'unknown', 'receiver type unknown and no method of this name in the tree');
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (all.length > this.maxInferred) {
|
|
2014
|
+
unresolved(name, 'unknown', `receiver type unknown; ${all.length} methods of this name exceed the bound`);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
for (const d of [...all].sort((a, b) => (a.id < b.id ? -1 : 1)))
|
|
2018
|
+
edge(d, 'inferred', name, 'bound by name alone');
|
|
2019
|
+
}
|
|
2020
|
+
run() {
|
|
2021
|
+
for (const ctx of this.sortedFiles())
|
|
2022
|
+
this.walk(ctx, ctx.root);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Call edges for a set of TypeScript/JavaScript files. Files of other
|
|
2027
|
+
* extensions are ignored. Deterministic: the same files give the same graph.
|
|
2028
|
+
*/
|
|
2029
|
+
export async function buildTsCallGraph(files, options = {}) {
|
|
2030
|
+
const graph = new Graph(options);
|
|
2031
|
+
const trees = [];
|
|
2032
|
+
const out = [];
|
|
2033
|
+
const partial = [];
|
|
2034
|
+
try {
|
|
2035
|
+
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
2036
|
+
for (const f of sorted) {
|
|
2037
|
+
const path = normalizePath(f.path.replace(/\\/g, '/'));
|
|
2038
|
+
const lang = languageOf(path);
|
|
2039
|
+
if (!lang)
|
|
2040
|
+
continue;
|
|
2041
|
+
const tree = await parse(f.code, lang.grammar);
|
|
2042
|
+
trees.push(tree);
|
|
2043
|
+
const ids = deriveTypeScriptIds(tree, path, lang.ids, f.ids ?? {});
|
|
2044
|
+
if (ids.partial)
|
|
2045
|
+
partial.push(path);
|
|
2046
|
+
out.push(ids);
|
|
2047
|
+
graph.add(path, tree, ids);
|
|
2048
|
+
}
|
|
2049
|
+
graph.link();
|
|
2050
|
+
graph.run();
|
|
2051
|
+
graph.manifestEntries();
|
|
2052
|
+
}
|
|
2053
|
+
finally {
|
|
2054
|
+
for (const t of trees)
|
|
2055
|
+
disposeTree(t);
|
|
2056
|
+
}
|
|
2057
|
+
const bySite = (a, b) => a.site.file < b.site.file ? -1 : a.site.file > b.site.file ? 1 : a.site.line - b.site.line || a.site.col - b.site.col;
|
|
2058
|
+
return {
|
|
2059
|
+
scheme: TS_CALL_EDGES_SCHEME,
|
|
2060
|
+
ids: 'ids@0.1',
|
|
2061
|
+
files: out,
|
|
2062
|
+
edges: [...graph.edges].sort((a, b) => bySite(a, b) || (a.to < b.to ? -1 : a.to > b.to ? 1 : 0)),
|
|
2063
|
+
unresolved: [...graph.unresolved].sort(bySite),
|
|
2064
|
+
partial,
|
|
2065
|
+
sites: graph.siteCount,
|
|
2066
|
+
references: dedupe([...graph.references].sort((a, b) => bySite(a, b) || cmp(a.to, b.to)), r => `${r.from}>${r.to}@${r.site.file}:${r.site.line}:${r.site.col}`),
|
|
2067
|
+
fieldUses: dedupe([...graph.fieldUses].sort((a, b) => bySite(a, b) || cmp(a.to, b.to)), r => `${r.from}>${r.to}@${r.site.file}:${r.site.line}:${r.site.col}`),
|
|
2068
|
+
imports: graph.importEdges(),
|
|
2069
|
+
loads: dedupe([...graph.loadEdges].sort((a, b) => bySite(a, b) || cmp(a.to, b.to)), l => `${l.from}>${l.to}`),
|
|
2070
|
+
typeOnly: [...graph.typeOnly].sort(),
|
|
2071
|
+
entryScheme: 'entry-index@0.1',
|
|
2072
|
+
entries: dedupe([...graph.entries].sort((a, b) => cmp(a.file, b.file) || a.line - b.line || cmp(a.id, b.id) || cmp(a.kind, b.kind) || cmp(a.path ?? '', b.path ?? '') || cmp(a.method ?? '', b.method ?? '')), e => `${e.id}|${e.kind}|${e.method ?? ''}|${e.path ?? ''}`),
|
|
2073
|
+
unresolvedRegistrations: [...graph.unresolvedRegistrations].sort(bySite),
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function cmp(a, b) {
|
|
2077
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2078
|
+
}
|
|
2079
|
+
function dedupe(items, key) {
|
|
2080
|
+
const seen = new Set();
|
|
2081
|
+
return items.filter(t => {
|
|
2082
|
+
const k = key(t);
|
|
2083
|
+
if (seen.has(k))
|
|
2084
|
+
return false;
|
|
2085
|
+
seen.add(k);
|
|
2086
|
+
return true;
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
//# sourceMappingURL=ts-call-edges.js.map
|