ucn 5.4.2 → 5.5.0
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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,1922 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/route-graph.js - AST router composition and constant propagation for
|
|
3
|
+
* `endpoints` (fix #366).
|
|
4
|
+
*
|
|
5
|
+
* Two pieces:
|
|
6
|
+
*
|
|
7
|
+
* 1. A string constant evaluator over tree-sitter ASTs. A route or mount
|
|
8
|
+
* prefix is often an expression, not a literal: `settings.API_V1_STR`
|
|
9
|
+
* (attribute of a module-level instance whose class field has a literal
|
|
10
|
+
* default), `f"{BASE}/v1"`, `API + "/users"`, `Paths.API` (Java/C#
|
|
11
|
+
* constant), a Go package constant. The evaluator folds literals,
|
|
12
|
+
* concatenation, template/f-strings, same-scope and module constants,
|
|
13
|
+
* imported constants, object-literal members and class field defaults.
|
|
14
|
+
* Anything it cannot prove becomes a DISCLOSED wildcard segment
|
|
15
|
+
* `{?expr}` instead of being dropped.
|
|
16
|
+
*
|
|
17
|
+
* 2. A router graph for the call-registered frameworks (JS/TS: Express,
|
|
18
|
+
* Koa, Hono, Fastify; Go: gin, echo, chi, fiber, gorilla/mux, net/http).
|
|
19
|
+
* Nodes are router VALUES identified by their declaring binding (scope
|
|
20
|
+
* aware: chi reuses `r` for every nested router), edges are mount calls
|
|
21
|
+
* with their prefix expressions (`use`, `route`, `register({prefix})`,
|
|
22
|
+
* `Group`, `Route`, `Mount`, `PathPrefix().Subrouter()`,
|
|
23
|
+
* `StripPrefix`), routers returned from functions and routers passed to
|
|
24
|
+
* function parameters. Composition walks the edges from each root, so a
|
|
25
|
+
* router mounted along several paths serves under several full paths.
|
|
26
|
+
*
|
|
27
|
+
* Everything here is advisory route inventory: an unresolved mount target
|
|
28
|
+
* contributes no edge (the route keeps the prefix it has), an unresolved
|
|
29
|
+
* prefix contributes a `{?expr}` segment.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const { getParser, safeParse } = require('../languages');
|
|
37
|
+
const { codeUnitCompare } = require('./shared');
|
|
38
|
+
|
|
39
|
+
const MAX_DEPTH = 8;
|
|
40
|
+
|
|
41
|
+
const JS_LANGS = new Set(['javascript', 'typescript', 'tsx']);
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// AST SESSION (one per endpoints extraction)
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
class AstSession {
|
|
48
|
+
constructor(index) {
|
|
49
|
+
this.index = index;
|
|
50
|
+
this.trees = new Map();
|
|
51
|
+
this.scopeDecls = new Map();
|
|
52
|
+
this.memo = new Map();
|
|
53
|
+
this.packageFiles = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
entry(file) { return this.index.files.get(file); }
|
|
57
|
+
|
|
58
|
+
lang(file) { return this.entry(file)?.language || null; }
|
|
59
|
+
|
|
60
|
+
/** Root node of a project file (parsed once per session). */
|
|
61
|
+
root(file) {
|
|
62
|
+
if (this.trees.has(file)) {
|
|
63
|
+
const tree = this.trees.get(file);
|
|
64
|
+
return tree ? tree.rootNode : null;
|
|
65
|
+
}
|
|
66
|
+
let tree = null;
|
|
67
|
+
const entry = this.entry(file);
|
|
68
|
+
if (entry) {
|
|
69
|
+
try {
|
|
70
|
+
const parser = getParser(entry.language);
|
|
71
|
+
if (parser) tree = safeParse(parser, this.text(file));
|
|
72
|
+
} catch (e) { tree = null; }
|
|
73
|
+
}
|
|
74
|
+
this.trees.set(file, tree);
|
|
75
|
+
return tree ? tree.rootNode : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Source text of a project file (read once per session). */
|
|
79
|
+
text(file) {
|
|
80
|
+
if (!this.texts) this.texts = new Map();
|
|
81
|
+
if (this.texts.has(file)) return this.texts.get(file);
|
|
82
|
+
let text;
|
|
83
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch (e) { text = ''; }
|
|
84
|
+
this.texts.set(file, text);
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Parse a detached snippet (annotation/attribute arguments). */
|
|
89
|
+
snippet(lang, code) {
|
|
90
|
+
const key = `snippet\0${lang}\0${code}`;
|
|
91
|
+
if (this.trees.has(key)) {
|
|
92
|
+
const tree = this.trees.get(key);
|
|
93
|
+
return tree ? tree.rootNode : null;
|
|
94
|
+
}
|
|
95
|
+
let tree = null;
|
|
96
|
+
try {
|
|
97
|
+
const parser = getParser(lang);
|
|
98
|
+
if (parser) tree = safeParse(parser, code);
|
|
99
|
+
} catch (e) { tree = null; }
|
|
100
|
+
this.trees.set(key, tree);
|
|
101
|
+
return tree ? tree.rootNode : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Memoize with a cycle guard: a re-entrant key answers `fallback`. */
|
|
105
|
+
once(key, fallback, fn) {
|
|
106
|
+
if (this.memo.has(key)) {
|
|
107
|
+
const value = this.memo.get(key);
|
|
108
|
+
return value === IN_PROGRESS ? fallback : value;
|
|
109
|
+
}
|
|
110
|
+
this.memo.set(key, IN_PROGRESS);
|
|
111
|
+
const value = fn();
|
|
112
|
+
this.memo.set(key, value);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Go files of one package directory (non-test first). */
|
|
117
|
+
goPackageFiles(dir) {
|
|
118
|
+
if (!this.packageFiles) {
|
|
119
|
+
this.packageFiles = new Map();
|
|
120
|
+
for (const [file, entry] of this.index.files) {
|
|
121
|
+
if (entry.language !== 'go') continue;
|
|
122
|
+
const d = path.dirname(file);
|
|
123
|
+
const list = this.packageFiles.get(d) || [];
|
|
124
|
+
list.push(file);
|
|
125
|
+
this.packageFiles.set(d, list);
|
|
126
|
+
}
|
|
127
|
+
for (const list of this.packageFiles.values()) list.sort(codeUnitCompare);
|
|
128
|
+
}
|
|
129
|
+
return this.packageFiles.get(dir) || [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const IN_PROGRESS = Symbol('in-progress');
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// SMALL AST HELPERS
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
function field(node, name) {
|
|
140
|
+
return node ? node.childForFieldName(name) : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function namedChildren(node) {
|
|
144
|
+
const out = [];
|
|
145
|
+
if (!node) return out;
|
|
146
|
+
for (let i = 0; i < node.namedChildCount; i++) out.push(node.namedChild(i));
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isComment(node) {
|
|
151
|
+
return node && node.type.includes('comment');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function unwrap(node) {
|
|
155
|
+
let n = node;
|
|
156
|
+
while (n && (n.type === 'parenthesized_expression' || n.type === 'as_expression' ||
|
|
157
|
+
n.type === 'satisfies_expression' || n.type === 'non_null_expression' ||
|
|
158
|
+
n.type === 'type_assertion' || n.type === 'await_expression')) {
|
|
159
|
+
const inner = namedChildren(n).find(c => !isComment(c));
|
|
160
|
+
if (!inner) break;
|
|
161
|
+
n = inner;
|
|
162
|
+
}
|
|
163
|
+
return n;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function nodeKey(file, node) {
|
|
167
|
+
return `${file}#${node.startIndex}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function unescapeSequence(text) {
|
|
171
|
+
switch (text) {
|
|
172
|
+
case '\\n': return '\n';
|
|
173
|
+
case '\\t': return '\t';
|
|
174
|
+
case '\\r': return '\r';
|
|
175
|
+
case '\\\\': return '\\';
|
|
176
|
+
case '\\"': return '"';
|
|
177
|
+
case "\\'": return "'";
|
|
178
|
+
case '\\/': return '/';
|
|
179
|
+
case '\\`': return '`';
|
|
180
|
+
default: return text.length === 2 ? text[1] : '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A disclosed unresolved segment. */
|
|
185
|
+
function unresolved(node) {
|
|
186
|
+
const text = String(node?.text ?? node ?? '')
|
|
187
|
+
.replace(/\s+/g, ' ').replace(/[{}/?]/g, '').trim().slice(0, 48);
|
|
188
|
+
return `{?${text || 'expr'}}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function hasUnresolved(value) {
|
|
192
|
+
return typeof value === 'string' && value.includes('{?');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const STRING_LITERAL_TYPES = new Set([
|
|
196
|
+
'string', 'template_string', 'concatenated_string',
|
|
197
|
+
'interpreted_string_literal', 'raw_string_literal',
|
|
198
|
+
'string_literal', 'verbatim_string_literal', 'interpolated_string_expression',
|
|
199
|
+
'raw_string_literal_expression', 'text_block',
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
function isStringLiteral(node) {
|
|
203
|
+
return !!node && STRING_LITERAL_TYPES.has(node.type);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Fold a string literal node; `evalChild` folds interpolated expressions. */
|
|
207
|
+
function readLiteral(node, evalChild) {
|
|
208
|
+
if (node.type === 'concatenated_string') {
|
|
209
|
+
return namedChildren(node).map(child => readLiteral(child, evalChild)).join('');
|
|
210
|
+
}
|
|
211
|
+
const parts = namedChildren(node);
|
|
212
|
+
// Python f-string literal text spells braces doubled (`{{id}}` is `{id}`).
|
|
213
|
+
const fstring = node.type === 'string' && parts[0]?.type === 'string_start' && /[fF]/.test(parts[0].text);
|
|
214
|
+
if (parts.length === 0) {
|
|
215
|
+
const m = node.text.match(/^[@$rRbBuUfF]*("""|'''|"|'|`)([\s\S]*)\1$/);
|
|
216
|
+
return m ? m[2] : '';
|
|
217
|
+
}
|
|
218
|
+
let out = '';
|
|
219
|
+
for (const part of parts) {
|
|
220
|
+
const t = part.type;
|
|
221
|
+
if (t === 'string_start' || t === 'string_end' || t === 'interpolation_start' ||
|
|
222
|
+
t === 'interpolation_brace' || t === 'interpolation_quote' ||
|
|
223
|
+
t === 'raw_string_start' || t === 'raw_string_end') continue;
|
|
224
|
+
if (t === 'escape_sequence') { out += unescapeSequence(part.text); continue; }
|
|
225
|
+
if (t === 'interpolation' || t === 'template_substitution') {
|
|
226
|
+
const inner = namedChildren(part).find(c => c.type !== 'interpolation_brace' &&
|
|
227
|
+
c.type !== 'format_specifier' && c.type !== 'type_conversion' && !isComment(c));
|
|
228
|
+
out += inner ? evalChild(inner) : unresolved(part);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (t.includes('content') || t.includes('fragment')) {
|
|
232
|
+
// Python string_content may embed escape_sequence children.
|
|
233
|
+
if (part.namedChildCount > 0) {
|
|
234
|
+
let s = '';
|
|
235
|
+
let cursor = part.startIndex;
|
|
236
|
+
for (const esc of namedChildren(part)) {
|
|
237
|
+
s += part.text.slice(cursor - part.startIndex, esc.startIndex - part.startIndex);
|
|
238
|
+
s += esc.type === 'escape_sequence' ? unescapeSequence(esc.text) : esc.text;
|
|
239
|
+
cursor = esc.endIndex;
|
|
240
|
+
}
|
|
241
|
+
s += part.text.slice(cursor - part.startIndex);
|
|
242
|
+
out += fstring ? s.replace(/\{\{/g, '{').replace(/\}\}/g, '}') : s;
|
|
243
|
+
} else {
|
|
244
|
+
out += fstring ? part.text.replace(/\{\{/g, '{').replace(/\}\}/g, '}') : part.text;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function binaryOperator(node) {
|
|
254
|
+
const op = field(node, 'operator');
|
|
255
|
+
if (op) return op.type === '+' ? '+' : op.text;
|
|
256
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
257
|
+
const c = node.child(i);
|
|
258
|
+
if (!c.isNamed) return c.type;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// CONSTANT EVALUATION
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Evaluate a string-valued expression. Returns the folded string; unprovable
|
|
269
|
+
* leaves appear as `{?expr}` segments.
|
|
270
|
+
*/
|
|
271
|
+
function evalString(sess, file, node, depth = 0, ctx = null) {
|
|
272
|
+
if (!node) return unresolved('');
|
|
273
|
+
if (depth > MAX_DEPTH) return unresolved(node);
|
|
274
|
+
const n = unwrap(node);
|
|
275
|
+
if (!n) return unresolved(node);
|
|
276
|
+
const recurse = child => evalString(sess, file, child, depth + 1, ctx);
|
|
277
|
+
|
|
278
|
+
if (isStringLiteral(n)) {
|
|
279
|
+
const value = readLiteral(n, recurse);
|
|
280
|
+
return value == null ? unresolved(n) : value;
|
|
281
|
+
}
|
|
282
|
+
if (n.type === 'binary_expression' || n.type === 'binary_operator') {
|
|
283
|
+
if (binaryOperator(n) !== '+') return unresolved(n);
|
|
284
|
+
const left = field(n, 'left');
|
|
285
|
+
const right = field(n, 'right');
|
|
286
|
+
if (!left || !right) return unresolved(n);
|
|
287
|
+
return recurse(left) + recurse(right);
|
|
288
|
+
}
|
|
289
|
+
// C# nameof(X) is the identifier text.
|
|
290
|
+
if (n.type === 'invocation_expression') {
|
|
291
|
+
const fn = field(n, 'function');
|
|
292
|
+
if (fn && fn.text === 'nameof') {
|
|
293
|
+
const arg = namedChildren(field(n, 'arguments'))[0];
|
|
294
|
+
if (arg) return arg.text.split('.').pop();
|
|
295
|
+
}
|
|
296
|
+
return unresolved(n);
|
|
297
|
+
}
|
|
298
|
+
const value = lookupValue(sess, file, n, depth + 1, ctx);
|
|
299
|
+
if (value && value.kind === 'expr') {
|
|
300
|
+
return evalString(sess, value.file, value.node, depth + 1,
|
|
301
|
+
value.ctx || (value.file === file ? ctx : null));
|
|
302
|
+
}
|
|
303
|
+
return unresolved(n);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Resolve a name/member expression to what it denotes:
|
|
308
|
+
* { kind: 'expr', file, node } - an expression (constant initializer)
|
|
309
|
+
* { kind: 'module', file, dir } - a module / Go package
|
|
310
|
+
* { kind: 'class', file, node } - a class declaration
|
|
311
|
+
* { kind: 'instance', cls } - an instance of a resolved class
|
|
312
|
+
* null - unknown
|
|
313
|
+
*/
|
|
314
|
+
function lookupValue(sess, file, node, depth, ctx = null) {
|
|
315
|
+
if (!node || depth > MAX_DEPTH) return null;
|
|
316
|
+
const n = unwrap(node);
|
|
317
|
+
const lang = ctx?.lang || sess.lang(file);
|
|
318
|
+
switch (n.type) {
|
|
319
|
+
case 'identifier':
|
|
320
|
+
case 'shorthand_property_identifier':
|
|
321
|
+
return lookupName(sess, file, n.text, n, depth, ctx);
|
|
322
|
+
case 'member_expression': // JS
|
|
323
|
+
return lookupMember(sess, file, field(n, 'object'), field(n, 'property')?.text, depth, ctx);
|
|
324
|
+
case 'attribute': // Python
|
|
325
|
+
return lookupMember(sess, file, field(n, 'object'), field(n, 'attribute')?.text, depth, ctx);
|
|
326
|
+
case 'selector_expression': // Go
|
|
327
|
+
return lookupMember(sess, file, field(n, 'operand'), field(n, 'field')?.text, depth, ctx);
|
|
328
|
+
case 'field_access': // Java
|
|
329
|
+
return lookupMember(sess, file, field(n, 'object'), field(n, 'field')?.text, depth, ctx);
|
|
330
|
+
case 'member_access_expression': // C#
|
|
331
|
+
return lookupMember(sess, file, field(n, 'expression'), field(n, 'name')?.text, depth, ctx);
|
|
332
|
+
case 'call_expression': { // JS: require('./consts')
|
|
333
|
+
const fn = field(n, 'function');
|
|
334
|
+
if (fn && fn.type === 'identifier' && fn.text === 'require') {
|
|
335
|
+
const arg = namedChildren(field(n, 'arguments'))[0];
|
|
336
|
+
const target = arg && arg.type === 'string'
|
|
337
|
+
? resolveJsSpecifier(sess, file, readLiteral(arg, () => '')) : null;
|
|
338
|
+
return target ? { kind: 'module', file: target } : null;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
case 'call': // Python: Settings()
|
|
343
|
+
case 'new_expression': { // JS: new Config()
|
|
344
|
+
const callee = field(n, n.type === 'call' ? 'function' : 'constructor');
|
|
345
|
+
const cls = lookupValue(sess, file, callee, depth + 1, ctx);
|
|
346
|
+
return cls && cls.kind === 'class' ? { kind: 'instance', cls } : null;
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
if (lang && isStringLiteral(n)) return { kind: 'expr', file, node: n, ctx };
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function lookupMember(sess, file, objNode, name, depth, ctx) {
|
|
355
|
+
if (!objNode || !name || depth > MAX_DEPTH) return null;
|
|
356
|
+
const obj = lookupValue(sess, file, objNode, depth + 1, ctx);
|
|
357
|
+
if (!obj) return null;
|
|
358
|
+
return memberOf(sess, obj, name, depth + 1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function memberOf(sess, obj, name, depth) {
|
|
362
|
+
if (depth > MAX_DEPTH || !obj) return null;
|
|
363
|
+
if (obj.kind === 'module') return lookupTopLevel(sess, obj.file, name, depth + 1, obj.dir);
|
|
364
|
+
if (obj.kind === 'class') return classMember(sess, obj.file, obj.node, name, depth + 1);
|
|
365
|
+
if (obj.kind === 'instance') {
|
|
366
|
+
return obj.cls ? classMember(sess, obj.cls.file, obj.cls.node, name, depth + 1) : null;
|
|
367
|
+
}
|
|
368
|
+
if (obj.kind === 'expr') {
|
|
369
|
+
const v = unwrap(obj.node);
|
|
370
|
+
if (v.type === 'object') { // JS object literal
|
|
371
|
+
for (const pair of namedChildren(v)) {
|
|
372
|
+
if (pair.type === 'pair') {
|
|
373
|
+
const key = field(pair, 'key');
|
|
374
|
+
const keyText = key && (key.type === 'string' ? readLiteral(key, () => '') : key.text);
|
|
375
|
+
if (keyText === name) return { kind: 'expr', file: obj.file, node: field(pair, 'value') };
|
|
376
|
+
} else if (pair.type === 'shorthand_property_identifier' && pair.text === name) {
|
|
377
|
+
return lookupName(sess, obj.file, name, pair, depth + 1);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const inner = lookupValue(sess, obj.file, v, depth + 1, obj.ctx);
|
|
383
|
+
if (inner && inner.kind !== 'expr') return memberOf(sess, inner, name, depth + 1);
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** A class member's constant initializer (class attribute, static field,
|
|
389
|
+
* const, or a pydantic/dataclass field default). */
|
|
390
|
+
function classMember(sess, file, classNode, name, depth) {
|
|
391
|
+
const body = field(classNode, 'body');
|
|
392
|
+
if (!body) return null;
|
|
393
|
+
const lang = sess.lang(file);
|
|
394
|
+
const hits = [];
|
|
395
|
+
const visitStatements = (container) => {
|
|
396
|
+
for (const stmt of namedChildren(container)) {
|
|
397
|
+
if (lang === 'python') {
|
|
398
|
+
const assign = stmt.type === 'expression_statement' ? stmt.namedChild(0) : stmt;
|
|
399
|
+
if (assign && assign.type === 'assignment') {
|
|
400
|
+
const left = field(assign, 'left');
|
|
401
|
+
const right = field(assign, 'right');
|
|
402
|
+
if (left && left.type === 'identifier' && left.text === name && right) hits.push(right);
|
|
403
|
+
}
|
|
404
|
+
} else if (JS_LANGS.has(lang)) {
|
|
405
|
+
if (stmt.type === 'public_field_definition' || stmt.type === 'field_definition') {
|
|
406
|
+
const prop = field(stmt, 'name') || field(stmt, 'property');
|
|
407
|
+
const value = field(stmt, 'value');
|
|
408
|
+
if (prop && prop.text === name && value) hits.push(value);
|
|
409
|
+
}
|
|
410
|
+
} else if (lang === 'java' || lang === 'csharp') {
|
|
411
|
+
if (stmt.type === 'field_declaration' || stmt.type === 'constant_declaration') {
|
|
412
|
+
const decls = [];
|
|
413
|
+
const collect = (n) => {
|
|
414
|
+
for (const c of namedChildren(n)) {
|
|
415
|
+
if (c.type === 'variable_declarator') decls.push(c);
|
|
416
|
+
else if (c.type === 'variable_declaration') collect(c);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
collect(stmt);
|
|
420
|
+
for (const d of decls) {
|
|
421
|
+
const nameNode = field(d, 'name') || d.namedChild(0);
|
|
422
|
+
if (!nameNode || nameNode.text !== name) continue;
|
|
423
|
+
const value = field(d, 'value') ||
|
|
424
|
+
namedChildren(d).filter(c => !(c.startIndex === nameNode.startIndex &&
|
|
425
|
+
c.endIndex === nameNode.endIndex)).pop();
|
|
426
|
+
if (value && value.type !== 'bracketed_argument_list') hits.push(value);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
visitStatements(body);
|
|
433
|
+
if (hits.length !== 1) return null;
|
|
434
|
+
return { kind: 'expr', file, node: hits[0], ctx: { lang, classNode } };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Scopes
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
const JS_FUNCTION_TYPES = new Set([
|
|
442
|
+
'function_declaration', 'function_expression', 'function', 'arrow_function',
|
|
443
|
+
'method_definition', 'generator_function_declaration', 'generator_function',
|
|
444
|
+
]);
|
|
445
|
+
const GO_FUNCTION_TYPES = new Set(['function_declaration', 'method_declaration', 'func_literal']);
|
|
446
|
+
const PY_FUNCTION_TYPES = new Set(['function_definition', 'lambda']);
|
|
447
|
+
// Go statements whose header declares variables scoped to the statement.
|
|
448
|
+
const GO_CLAUSE_SCOPES = new Set([
|
|
449
|
+
'for_statement', 'if_statement', 'expression_switch_statement', 'type_switch_statement',
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
function isScopeNode(lang, node) {
|
|
453
|
+
if (JS_LANGS.has(lang)) {
|
|
454
|
+
return node.type === 'program' || node.type === 'statement_block' ||
|
|
455
|
+
JS_FUNCTION_TYPES.has(node.type);
|
|
456
|
+
}
|
|
457
|
+
if (lang === 'go') {
|
|
458
|
+
return node.type === 'source_file' || node.type === 'block' ||
|
|
459
|
+
GO_FUNCTION_TYPES.has(node.type) || GO_CLAUSE_SCOPES.has(node.type);
|
|
460
|
+
}
|
|
461
|
+
if (lang === 'python') {
|
|
462
|
+
return node.type === 'module' || PY_FUNCTION_TYPES.has(node.type);
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Parameter name nodes of a function node, in positional order. */
|
|
468
|
+
function paramNames(lang, fnNode) {
|
|
469
|
+
const out = [];
|
|
470
|
+
const params = field(fnNode, 'parameters') ||
|
|
471
|
+
(fnNode.type === 'arrow_function' ? field(fnNode, 'parameter') : null);
|
|
472
|
+
if (!params) return out;
|
|
473
|
+
if (params.type === 'identifier') return [{ name: params, type: null }];
|
|
474
|
+
for (const p of namedChildren(params)) {
|
|
475
|
+
if (isComment(p)) continue;
|
|
476
|
+
if (JS_LANGS.has(lang)) {
|
|
477
|
+
if (p.type === 'identifier') out.push({ name: p, type: null });
|
|
478
|
+
else if (p.type === 'required_parameter' || p.type === 'optional_parameter') {
|
|
479
|
+
const pat = field(p, 'pattern');
|
|
480
|
+
out.push({ name: pat && pat.type === 'identifier' ? pat : null, type: field(p, 'type') });
|
|
481
|
+
} else if (p.type === 'assignment_pattern') {
|
|
482
|
+
const left = field(p, 'left');
|
|
483
|
+
out.push({ name: left && left.type === 'identifier' ? left : null, type: null });
|
|
484
|
+
} else {
|
|
485
|
+
out.push({ name: null, type: null });
|
|
486
|
+
}
|
|
487
|
+
} else if (lang === 'go') {
|
|
488
|
+
if (p.type !== 'parameter_declaration' && p.type !== 'variadic_parameter_declaration') continue;
|
|
489
|
+
const names = [];
|
|
490
|
+
for (let i = 0; i < p.childCount; i++) {
|
|
491
|
+
if (p.fieldNameForChild(i) === 'name') names.push(p.child(i));
|
|
492
|
+
}
|
|
493
|
+
const type = field(p, 'type');
|
|
494
|
+
if (names.length === 0) out.push({ name: null, type });
|
|
495
|
+
for (const nm of names) out.push({ name: nm, type });
|
|
496
|
+
} else if (lang === 'python') {
|
|
497
|
+
if (p.type === 'identifier') out.push({ name: p, type: null });
|
|
498
|
+
else if (p.type === 'typed_parameter') {
|
|
499
|
+
const nm = namedChildren(p).find(c => c.type === 'identifier');
|
|
500
|
+
out.push({ name: nm || null, type: field(p, 'type') });
|
|
501
|
+
} else if (p.type === 'default_parameter' || p.type === 'typed_default_parameter') {
|
|
502
|
+
out.push({ name: field(p, 'name'), type: field(p, 'type') });
|
|
503
|
+
} else {
|
|
504
|
+
out.push({ name: null, type: null });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Declarations directly owned by a scope node: Map name -> decl record
|
|
513
|
+
* { nameNode, valueNode, kind, count, typeNode, fnNode, paramIndex }
|
|
514
|
+
*/
|
|
515
|
+
function scopeDecls(sess, file, lang, scope) {
|
|
516
|
+
const key = `${file}#${scope.startIndex}:${scope.type}`;
|
|
517
|
+
const cached = sess.scopeDecls.get(key);
|
|
518
|
+
if (cached) return cached;
|
|
519
|
+
const map = new Map();
|
|
520
|
+
const add = (nameNode, rec) => {
|
|
521
|
+
if (!nameNode) return;
|
|
522
|
+
const prev = map.get(nameNode.text);
|
|
523
|
+
if (prev) { prev.count++; return; }
|
|
524
|
+
map.set(nameNode.text, { nameNode, count: 1, ...rec });
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const isFn = JS_LANGS.has(lang) ? JS_FUNCTION_TYPES.has(scope.type)
|
|
528
|
+
: lang === 'go' ? GO_FUNCTION_TYPES.has(scope.type)
|
|
529
|
+
: PY_FUNCTION_TYPES.has(scope.type);
|
|
530
|
+
if (lang === 'go' && GO_CLAUSE_SCOPES.has(scope.type)) {
|
|
531
|
+
// `for k, v := range x`, `if v := f(); ...`, `switch x := y.(type)`
|
|
532
|
+
for (const c of namedChildren(scope)) {
|
|
533
|
+
if (c.type === 'range_clause') {
|
|
534
|
+
for (const nm of namedChildren(field(c, 'left'))) {
|
|
535
|
+
if (nm.type === 'identifier') add(nm, { kind: 'var', valueNode: null });
|
|
536
|
+
}
|
|
537
|
+
} else if (c.type === 'for_clause') {
|
|
538
|
+
const init = field(c, 'initializer');
|
|
539
|
+
if (init) collectStatement(init);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const init = field(scope, 'initializer');
|
|
543
|
+
if (init) collectStatement(init);
|
|
544
|
+
const alias = field(scope, 'alias');
|
|
545
|
+
if (alias) {
|
|
546
|
+
const names = alias.type === 'identifier' ? [alias] : namedChildren(alias);
|
|
547
|
+
for (const nm of names) if (nm.type === 'identifier') add(nm, { kind: 'var', valueNode: null });
|
|
548
|
+
}
|
|
549
|
+
} else if (isFn) {
|
|
550
|
+
paramNames(lang, scope).forEach((p, i) => {
|
|
551
|
+
if (p.name) add(p.name, { kind: 'param', valueNode: null, typeNode: p.type, fnNode: scope, paramIndex: i });
|
|
552
|
+
});
|
|
553
|
+
if (lang === 'go') {
|
|
554
|
+
const recv = field(scope, 'receiver');
|
|
555
|
+
for (const p of namedChildren(recv)) {
|
|
556
|
+
const nm = field(p, 'name');
|
|
557
|
+
if (nm) add(nm, { kind: 'receiver', valueNode: null, typeNode: field(p, 'type') });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (lang === 'python' && scope.type === 'function_definition') {
|
|
561
|
+
const body = field(scope, 'body');
|
|
562
|
+
if (body) collectStatements(body);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
collectStatements(scope);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function collectStatements(container) {
|
|
569
|
+
for (const stmt of namedChildren(container)) {
|
|
570
|
+
if (stmt.type === 'statement_list') { collectStatements(stmt); continue; }
|
|
571
|
+
collectStatement(stmt);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function collectStatement(stmt) {
|
|
576
|
+
if (JS_LANGS.has(lang)) {
|
|
577
|
+
if (stmt.type === 'export_statement') {
|
|
578
|
+
const decl = field(stmt, 'declaration');
|
|
579
|
+
if (decl) collectStatement(decl);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') {
|
|
583
|
+
const isConst = stmt.type === 'lexical_declaration' && stmt.child(0)?.text === 'const';
|
|
584
|
+
for (const d of namedChildren(stmt)) {
|
|
585
|
+
if (d.type !== 'variable_declarator') continue;
|
|
586
|
+
const nm = field(d, 'name');
|
|
587
|
+
if (nm && nm.type === 'identifier') {
|
|
588
|
+
add(nm, { kind: isConst ? 'const' : 'var', valueNode: field(d, 'value') });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (stmt.type === 'function_declaration' || stmt.type === 'generator_function_declaration') {
|
|
594
|
+
add(field(stmt, 'name'), { kind: 'function', valueNode: stmt });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (stmt.type === 'class_declaration') {
|
|
598
|
+
add(field(stmt, 'name'), { kind: 'class', valueNode: stmt });
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (lang === 'go') {
|
|
603
|
+
if (stmt.type === 'short_var_declaration') {
|
|
604
|
+
const left = namedChildren(field(stmt, 'left'));
|
|
605
|
+
const right = namedChildren(field(stmt, 'right'));
|
|
606
|
+
left.forEach((nm, i) => {
|
|
607
|
+
if (nm.type === 'identifier' && nm.text !== '_') {
|
|
608
|
+
add(nm, { kind: 'var', valueNode: left.length === right.length ? right[i] : null });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (stmt.type === 'var_declaration' || stmt.type === 'const_declaration') {
|
|
614
|
+
const specs = [];
|
|
615
|
+
const walkSpecs = (n) => {
|
|
616
|
+
for (const c of namedChildren(n)) {
|
|
617
|
+
if (c.type === 'var_spec' || c.type === 'const_spec') specs.push(c);
|
|
618
|
+
else if (c.type === 'var_spec_list' || c.type === 'const_spec_list') walkSpecs(c);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
walkSpecs(stmt);
|
|
622
|
+
for (const spec of specs) {
|
|
623
|
+
const names = [];
|
|
624
|
+
for (let i = 0; i < spec.childCount; i++) {
|
|
625
|
+
if (spec.fieldNameForChild(i) === 'name') names.push(spec.child(i));
|
|
626
|
+
}
|
|
627
|
+
const values = namedChildren(field(spec, 'value'));
|
|
628
|
+
names.forEach((nm, i) => add(nm, {
|
|
629
|
+
kind: stmt.type === 'const_declaration' ? 'const' : 'var',
|
|
630
|
+
valueNode: names.length === values.length ? values[i] : null,
|
|
631
|
+
typeNode: field(spec, 'type'),
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (stmt.type === 'function_declaration') {
|
|
637
|
+
add(field(stmt, 'name'), { kind: 'function', valueNode: stmt });
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (lang === 'python') {
|
|
642
|
+
if (stmt.type === 'expression_statement') {
|
|
643
|
+
const assign = stmt.namedChild(0);
|
|
644
|
+
if (assign && assign.type === 'assignment') {
|
|
645
|
+
const left = field(assign, 'left');
|
|
646
|
+
if (left && left.type === 'identifier') {
|
|
647
|
+
add(left, { kind: 'var', valueNode: field(assign, 'right') });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (stmt.type === 'decorated_definition') {
|
|
653
|
+
const def = field(stmt, 'definition');
|
|
654
|
+
if (def) collectStatement(def);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (stmt.type === 'class_definition') {
|
|
658
|
+
add(field(stmt, 'name'), { kind: 'class', valueNode: stmt });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (stmt.type === 'function_definition') {
|
|
662
|
+
add(field(stmt, 'name'), { kind: 'function', valueNode: stmt });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
sess.scopeDecls.set(key, map);
|
|
668
|
+
return map;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Nearest lexical declaration of `name` visible from `refNode`. */
|
|
672
|
+
function findDecl(sess, file, name, refNode) {
|
|
673
|
+
const lang = sess.lang(file);
|
|
674
|
+
let scope = refNode ? refNode.parent : sess.root(file);
|
|
675
|
+
while (scope) {
|
|
676
|
+
if (isScopeNode(lang, scope)) {
|
|
677
|
+
const decl = scopeDecls(sess, file, lang, scope).get(name);
|
|
678
|
+
// A declaration in the same scope that starts after the reference
|
|
679
|
+
// is not visible to it (Go/Python statement order); JS hoisting
|
|
680
|
+
// of function declarations is honored.
|
|
681
|
+
if (decl && (!refNode || decl.kind === 'function' || decl.kind === 'param' ||
|
|
682
|
+
decl.kind === 'class' || decl.nameNode.startIndex <= refNode.startIndex ||
|
|
683
|
+
scope.parent === null)) {
|
|
684
|
+
return { ...decl, scope };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
scope = scope.parent;
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function declValue(sess, file, decl, depth) {
|
|
693
|
+
if (!decl) return null;
|
|
694
|
+
if (decl.kind === 'class') return { kind: 'class', file, node: decl.valueNode };
|
|
695
|
+
if (decl.kind === 'function' || decl.kind === 'param' || decl.kind === 'receiver') return null;
|
|
696
|
+
if (decl.count > 1 || !decl.valueNode) return null;
|
|
697
|
+
// JS let/var and Go var are reassignable; a single declaration with an
|
|
698
|
+
// initializer is still the only value the scan can see, and Python has
|
|
699
|
+
// no const at all, so a single assignment is the declared value.
|
|
700
|
+
return { kind: 'expr', file, node: decl.valueNode };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function lookupName(sess, file, name, refNode, depth, ctx = null) {
|
|
704
|
+
if (depth > MAX_DEPTH) return null;
|
|
705
|
+
const lang = ctx?.lang || sess.lang(file);
|
|
706
|
+
if (lang === 'java' || lang === 'csharp') return lookupJvmName(sess, file, name, depth, ctx);
|
|
707
|
+
|
|
708
|
+
const decl = findDecl(sess, file, name, refNode);
|
|
709
|
+
if (decl) return declValue(sess, file, decl, depth);
|
|
710
|
+
|
|
711
|
+
if (lang === 'go') {
|
|
712
|
+
// An import alias (`http.MethodGet`) never needs the package scan.
|
|
713
|
+
const entry = sess.entry(file);
|
|
714
|
+
if ((entry?.importBindings || []).some(b => b.name === name)) return lookupImport(sess, file, name, depth);
|
|
715
|
+
return lookupGoPackageLevel(sess, path.dirname(file), name, depth, file);
|
|
716
|
+
}
|
|
717
|
+
return lookupImport(sess, file, name, depth);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Top-level name in a module (or Go package directory). */
|
|
721
|
+
function lookupTopLevel(sess, file, name, depth, dir = null) {
|
|
722
|
+
if (depth > MAX_DEPTH) return null;
|
|
723
|
+
const lang = sess.lang(file);
|
|
724
|
+
if (lang === 'go') return lookupGoPackageLevel(sess, dir || path.dirname(file), name, depth, null);
|
|
725
|
+
const root = sess.root(file);
|
|
726
|
+
if (!root) return null;
|
|
727
|
+
const decl = scopeDecls(sess, file, lang, root).get(name);
|
|
728
|
+
if (decl) return declValue(sess, file, decl, depth);
|
|
729
|
+
if (JS_LANGS.has(lang)) {
|
|
730
|
+
const exported = resolveJsExport(sess, file, name, depth + 1);
|
|
731
|
+
if (exported && exported.file !== file) return exported.value;
|
|
732
|
+
}
|
|
733
|
+
return lookupImport(sess, file, name, depth);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function lookupGoPackageLevel(sess, dir, name, depth, skipFile) {
|
|
737
|
+
for (const f of sess.goPackageFiles(dir)) {
|
|
738
|
+
if (f === skipFile) continue;
|
|
739
|
+
// A file that never spells the name cannot declare it (skip the parse).
|
|
740
|
+
if (!sess.text(f).includes(name)) continue;
|
|
741
|
+
const root = sess.root(f);
|
|
742
|
+
if (!root) continue;
|
|
743
|
+
const decl = scopeDecls(sess, f, 'go', root).get(name);
|
|
744
|
+
if (decl) return declValue(sess, f, decl, depth);
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Resolve a name bound by an import statement. */
|
|
750
|
+
function lookupImport(sess, file, name, depth) {
|
|
751
|
+
const entry = sess.entry(file);
|
|
752
|
+
if (!entry || depth > MAX_DEPTH) return null;
|
|
753
|
+
const lang = entry.language;
|
|
754
|
+
const bindings = entry.importBindings || [];
|
|
755
|
+
const local = name;
|
|
756
|
+
let binding = bindings.find(b => (b.alias || b.name) === local);
|
|
757
|
+
let original = binding ? binding.name : name;
|
|
758
|
+
if (!binding) {
|
|
759
|
+
const alias = (entry.importAliases || []).find(a => a.local === local);
|
|
760
|
+
if (alias) {
|
|
761
|
+
original = alias.original;
|
|
762
|
+
binding = bindings.find(b => b.name === original);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (!binding) return null;
|
|
766
|
+
const resolvedRel = (spec) => entry.moduleResolved?.[spec];
|
|
767
|
+
const abs = rel => path.join(sess.index.root, rel);
|
|
768
|
+
|
|
769
|
+
if (lang === 'go') {
|
|
770
|
+
const rel = resolvedRel(binding.module);
|
|
771
|
+
return rel ? { kind: 'module', file: abs(rel), dir: path.dirname(abs(rel)) } : null;
|
|
772
|
+
}
|
|
773
|
+
if (lang === 'python') {
|
|
774
|
+
const mod = String(binding.module || '');
|
|
775
|
+
if (binding.kind === 'from') {
|
|
776
|
+
const subSpec = mod.endsWith('.') ? mod + original : `${mod}.${original}`;
|
|
777
|
+
const subRel = resolvedRel(subSpec);
|
|
778
|
+
if (subRel) return { kind: 'module', file: abs(subRel) };
|
|
779
|
+
const rel = resolvedRel(mod);
|
|
780
|
+
return rel ? lookupTopLevel(sess, abs(rel), original, depth + 1) : null;
|
|
781
|
+
}
|
|
782
|
+
const rel = resolvedRel(mod);
|
|
783
|
+
return rel ? { kind: 'module', file: abs(rel) } : null;
|
|
784
|
+
}
|
|
785
|
+
if (JS_LANGS.has(lang)) {
|
|
786
|
+
const rel = resolvedRel(binding.module);
|
|
787
|
+
if (!rel) return null;
|
|
788
|
+
const target = abs(rel);
|
|
789
|
+
if (binding.kind === 'namespace' || original === '*' ||
|
|
790
|
+
(binding.kind === 'require' && binding.defaultLike)) {
|
|
791
|
+
return { kind: 'module', file: target };
|
|
792
|
+
}
|
|
793
|
+
const exported = jsImportedRecord(sess, target, binding, original, depth + 1);
|
|
794
|
+
return exported ? exported.value : null;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// JS module exports (ESM + CommonJS), AST-level
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Resolve an exported NAME of a JS/TS module to its declaration.
|
|
805
|
+
* Returns { file, declNode|null, valueNode|null, value } where `value` is a
|
|
806
|
+
* lookupValue-style descriptor for constant evaluation.
|
|
807
|
+
*/
|
|
808
|
+
function resolveJsExport(sess, file, name, depth) {
|
|
809
|
+
if (depth > MAX_DEPTH) return null;
|
|
810
|
+
return sess.once(`jsexport\0${file}\0${name}`, null, () => {
|
|
811
|
+
const root = sess.root(file);
|
|
812
|
+
if (!root) return null;
|
|
813
|
+
const lang = sess.lang(file);
|
|
814
|
+
const decls = scopeDecls(sess, file, lang, root);
|
|
815
|
+
const local = (localName) => {
|
|
816
|
+
const d = decls.get(localName);
|
|
817
|
+
if (d) {
|
|
818
|
+
return { file, declNode: d.nameNode, valueNode: d.valueNode, decl: d,
|
|
819
|
+
value: declValue(sess, file, d, depth) };
|
|
820
|
+
}
|
|
821
|
+
const imported = lookupImport(sess, file, localName, depth + 1);
|
|
822
|
+
return imported ? { file, declNode: null, valueNode: null, value: imported,
|
|
823
|
+
importedName: localName } : null;
|
|
824
|
+
};
|
|
825
|
+
for (const stmt of namedChildren(root)) {
|
|
826
|
+
if (stmt.type === 'export_statement') {
|
|
827
|
+
const decl = field(stmt, 'declaration');
|
|
828
|
+
if (decl) {
|
|
829
|
+
for (const d of declaredNames(decl)) if (d === name) return local(name);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const source = field(stmt, 'source');
|
|
833
|
+
const clause = namedChildren(stmt).find(c => c.type === 'export_clause');
|
|
834
|
+
if (clause) {
|
|
835
|
+
for (const spec of namedChildren(clause)) {
|
|
836
|
+
if (spec.type !== 'export_specifier') continue;
|
|
837
|
+
const orig = field(spec, 'name')?.text;
|
|
838
|
+
const alias = field(spec, 'alias')?.text || orig;
|
|
839
|
+
if (alias !== name || !orig) continue;
|
|
840
|
+
if (source) {
|
|
841
|
+
const target = resolveJsSpecifier(sess, file, readLiteral(source, () => ''));
|
|
842
|
+
return target ? resolveJsExport(sess, target, orig, depth + 1) : null;
|
|
843
|
+
}
|
|
844
|
+
return local(orig);
|
|
845
|
+
}
|
|
846
|
+
} else if (source && /\*/.test(stmt.text.slice(0, stmt.text.indexOf('from')))) {
|
|
847
|
+
const target = resolveJsSpecifier(sess, file, readLiteral(source, () => ''));
|
|
848
|
+
const hit = target && resolveJsExport(sess, target, name, depth + 1);
|
|
849
|
+
if (hit) return hit;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// CommonJS: module.exports = { name }, module.exports.name = X, exports.name = X
|
|
853
|
+
const assign = stmt.type === 'expression_statement' ? stmt.namedChild(0) : null;
|
|
854
|
+
if (assign && assign.type === 'assignment_expression') {
|
|
855
|
+
const left = field(assign, 'left');
|
|
856
|
+
const right = field(assign, 'right');
|
|
857
|
+
if (!left || !right) continue;
|
|
858
|
+
const lt = left.text.replace(/\s+/g, '');
|
|
859
|
+
if ((lt === `module.exports.${name}` || lt === `exports.${name}`)) {
|
|
860
|
+
return jsValueRecord(sess, file, right, depth);
|
|
861
|
+
}
|
|
862
|
+
if (lt === 'module.exports' && unwrap(right).type === 'object') {
|
|
863
|
+
for (const pair of namedChildren(unwrap(right))) {
|
|
864
|
+
if (pair.type === 'pair' && field(pair, 'key')?.text === name) {
|
|
865
|
+
return jsValueRecord(sess, file, field(pair, 'value'), depth);
|
|
866
|
+
}
|
|
867
|
+
if (pair.type === 'shorthand_property_identifier' && pair.text === name) {
|
|
868
|
+
return local(name);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return null;
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function declaredNames(decl) {
|
|
879
|
+
const out = [];
|
|
880
|
+
if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
|
|
881
|
+
for (const d of namedChildren(decl)) {
|
|
882
|
+
if (d.type === 'variable_declarator' && field(d, 'name')?.type === 'identifier') {
|
|
883
|
+
out.push(field(d, 'name').text);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
const nm = field(decl, 'name');
|
|
888
|
+
if (nm) out.push(nm.text);
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** An expression value inside a module; identifiers resolve to their decl. */
|
|
894
|
+
function jsValueRecord(sess, file, node, depth) {
|
|
895
|
+
const v = unwrap(node);
|
|
896
|
+
if (v && v.type === 'identifier') {
|
|
897
|
+
const d = findDecl(sess, file, v.text, v);
|
|
898
|
+
if (d) {
|
|
899
|
+
return { file, declNode: d.nameNode, valueNode: d.valueNode, decl: d,
|
|
900
|
+
value: declValue(sess, file, d, depth) };
|
|
901
|
+
}
|
|
902
|
+
const imported = lookupImport(sess, file, v.text, depth + 1);
|
|
903
|
+
return imported ? { file, declNode: null, valueNode: null, value: imported, importedName: v.text } : null;
|
|
904
|
+
}
|
|
905
|
+
return { file, declNode: null, valueNode: v, value: v ? { kind: 'expr', file, node: v } : null };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** The default export (`export default X`, `module.exports = X`). */
|
|
909
|
+
function resolveJsDefaultExport(sess, file, depth) {
|
|
910
|
+
if (depth > MAX_DEPTH) return null;
|
|
911
|
+
return sess.once(`jsdefault\0${file}`, null, () => {
|
|
912
|
+
const root = sess.root(file);
|
|
913
|
+
if (!root) return null;
|
|
914
|
+
for (const stmt of namedChildren(root)) {
|
|
915
|
+
if (stmt.type === 'export_statement' && /^export\s+default\b/.test(stmt.text)) {
|
|
916
|
+
const decl = field(stmt, 'declaration');
|
|
917
|
+
if (decl) {
|
|
918
|
+
const nm = field(decl, 'name');
|
|
919
|
+
return nm ? { file, declNode: nm, valueNode: decl, value: null }
|
|
920
|
+
: { file, declNode: null, valueNode: decl, value: null };
|
|
921
|
+
}
|
|
922
|
+
const value = field(stmt, 'value') || namedChildren(stmt).find(c => !isComment(c));
|
|
923
|
+
return value ? jsValueRecord(sess, file, value, depth) : null;
|
|
924
|
+
}
|
|
925
|
+
const assign = stmt.type === 'expression_statement' ? stmt.namedChild(0) : null;
|
|
926
|
+
if (assign && assign.type === 'assignment_expression' &&
|
|
927
|
+
field(assign, 'left')?.text.replace(/\s+/g, '') === 'module.exports') {
|
|
928
|
+
return jsValueRecord(sess, file, field(assign, 'right'), depth);
|
|
929
|
+
}
|
|
930
|
+
// `var app = module.exports = express()` (fix #397): the export
|
|
931
|
+
// is the declared binding's value.
|
|
932
|
+
if (stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') {
|
|
933
|
+
for (const declarator of namedChildren(stmt)) {
|
|
934
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
935
|
+
const nameNode = field(declarator, 'name');
|
|
936
|
+
for (let v = unwrap(field(declarator, 'value')); v?.type === 'assignment_expression';
|
|
937
|
+
v = unwrap(field(v, 'right'))) {
|
|
938
|
+
if (field(v, 'left')?.text.replace(/\s+/g, '') !== 'module.exports') continue;
|
|
939
|
+
if (nameNode?.type === 'identifier') return jsValueRecord(sess, file, nameNode, depth);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// `export { x as default }` / `export { default } from './y'`
|
|
945
|
+
return resolveJsExport(sess, file, 'default', depth + 1);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** The export an import binding denotes (default vs named, with the
|
|
950
|
+
* default-import-labeled-named fallback). */
|
|
951
|
+
function jsImportedRecord(sess, target, binding, original, depth) {
|
|
952
|
+
if (binding.kind === 'default' || original === 'default' ||
|
|
953
|
+
(binding.kind === 'require' && binding.defaultLike)) {
|
|
954
|
+
return resolveJsDefaultExport(sess, target, depth);
|
|
955
|
+
}
|
|
956
|
+
return resolveJsExport(sess, target, original, depth) ||
|
|
957
|
+
(binding.kind === 'named' ? resolveJsDefaultExport(sess, target, depth) : null);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function resolveJsSpecifier(sess, file, spec) {
|
|
961
|
+
const entry = sess.entry(file);
|
|
962
|
+
const rel = entry?.moduleResolved?.[spec];
|
|
963
|
+
return rel ? path.join(sess.index.root, rel) : null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ---------------------------------------------------------------------------
|
|
967
|
+
// Java / C# constants
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
|
|
970
|
+
function findClassDecls(root, name) {
|
|
971
|
+
const out = [];
|
|
972
|
+
const visit = (node) => {
|
|
973
|
+
if (/^(class|interface|enum|record|struct)_declaration$/.test(node.type)) {
|
|
974
|
+
const nm = field(node, 'name');
|
|
975
|
+
if (nm && nm.text === name) out.push(node);
|
|
976
|
+
}
|
|
977
|
+
for (const c of namedChildren(node)) {
|
|
978
|
+
if (c.type.endsWith('_declaration') || c.type === 'class_body' || c.type === 'declaration_list' ||
|
|
979
|
+
c.type === 'program' || c.type === 'compilation_unit' || c.type === 'namespace_declaration' ||
|
|
980
|
+
c.type === 'file_scoped_namespace_declaration' || c.type === 'enum_body' ||
|
|
981
|
+
c.type === 'interface_body') visit(c);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
visit(root);
|
|
985
|
+
return out;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Resolve a Java/C# class by simple name: same file, same directory, then
|
|
989
|
+
* a unique project-wide definition. */
|
|
990
|
+
function resolveJvmClass(sess, file, name) {
|
|
991
|
+
return sess.once(`jvmclass\0${file}\0${name}`, null, () => {
|
|
992
|
+
const root = sess.root(file);
|
|
993
|
+
const here = root ? findClassDecls(root, name) : [];
|
|
994
|
+
if (here.length === 1) return { kind: 'class', file, node: here[0] };
|
|
995
|
+
const defs = (sess.index.symbols.get(name) || []).filter(d =>
|
|
996
|
+
/^(class|interface|enum|record|struct)$/.test(d.type) &&
|
|
997
|
+
['java', 'csharp'].includes(sess.lang(d.file)));
|
|
998
|
+
const dir = path.dirname(file);
|
|
999
|
+
const sameDir = defs.filter(d => path.dirname(d.file) === dir);
|
|
1000
|
+
const pick = sameDir.length === 1 ? sameDir[0] : defs.length === 1 ? defs[0] : null;
|
|
1001
|
+
if (!pick) return null;
|
|
1002
|
+
const r = sess.root(pick.file);
|
|
1003
|
+
const nodes = r ? findClassDecls(r, name) : [];
|
|
1004
|
+
return nodes.length === 1 ? { kind: 'class', file: pick.file, node: nodes[0] } : null;
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function lookupJvmName(sess, file, name, depth, ctx) {
|
|
1009
|
+
// A bare identifier: a field of the enclosing class, else a class name.
|
|
1010
|
+
if (ctx?.classNode) {
|
|
1011
|
+
const member = classMember(sess, file, ctx.classNode, name, depth + 1);
|
|
1012
|
+
if (member) return member;
|
|
1013
|
+
}
|
|
1014
|
+
return resolveJvmClass(sess, file, name);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
// ROUTER GRAPH (JS/TS + Go)
|
|
1019
|
+
// ============================================================================
|
|
1020
|
+
|
|
1021
|
+
const JS_ROUTE_VERBS = new Set(['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all']);
|
|
1022
|
+
const GO_ROUTE_VERBS = new Set([
|
|
1023
|
+
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'Any', 'Handle', 'HandleFunc',
|
|
1024
|
+
'Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options', 'Connect', 'Trace', 'All',
|
|
1025
|
+
]);
|
|
1026
|
+
|
|
1027
|
+
// Router factories: the call that creates a router value (framework API).
|
|
1028
|
+
const JS_FACTORY_CALLS = new Set(['express', 'Router', 'fastify', 'Fastify', 'Koa']);
|
|
1029
|
+
const JS_FACTORY_CTORS = new Set(['Router', 'Hono', 'Koa', 'Fastify']);
|
|
1030
|
+
const GO_FACTORY_CALLS = new Set([
|
|
1031
|
+
'chi.NewRouter', 'chi.NewMux', 'gin.New', 'gin.Default', 'echo.New', 'fiber.New',
|
|
1032
|
+
'mux.NewRouter', 'http.NewServeMux', 'httprouter.New',
|
|
1033
|
+
]);
|
|
1034
|
+
// Parameter types that denote a router value.
|
|
1035
|
+
const GO_ROUTER_TYPES = new Set([
|
|
1036
|
+
'chi.Router', 'chi.Mux', 'chi.Routes', 'gin.Engine', 'gin.RouterGroup', 'gin.IRouter',
|
|
1037
|
+
'gin.IRoutes', 'echo.Echo', 'echo.Group', 'fiber.Router', 'fiber.App', 'fiber.Group',
|
|
1038
|
+
'mux.Router', 'http.ServeMux',
|
|
1039
|
+
]);
|
|
1040
|
+
const JS_ROUTER_TYPES = new Set([
|
|
1041
|
+
'Router', 'Express', 'Application', 'FastifyInstance', 'Hono', 'Koa', 'express.Router',
|
|
1042
|
+
'express.Express', 'express.Application',
|
|
1043
|
+
]);
|
|
1044
|
+
|
|
1045
|
+
function goTypeName(typeNode) {
|
|
1046
|
+
if (!typeNode) return null;
|
|
1047
|
+
let t = typeNode;
|
|
1048
|
+
while (t && (t.type === 'pointer_type' || t.type === 'parenthesized_type')) t = t.namedChild(0);
|
|
1049
|
+
if (!t) return null;
|
|
1050
|
+
if (t.type === 'qualified_type') {
|
|
1051
|
+
return `${field(t, 'package')?.text}.${field(t, 'name')?.text}`;
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function jsTypeName(typeNode) {
|
|
1057
|
+
if (!typeNode) return null;
|
|
1058
|
+
const text = typeNode.text.replace(/^:\s*/, '').trim();
|
|
1059
|
+
const m = text.match(/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)/);
|
|
1060
|
+
return m ? m[1] : null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
class RouteGraph {
|
|
1064
|
+
constructor(index, sess) {
|
|
1065
|
+
this.index = index;
|
|
1066
|
+
this.sess = sess;
|
|
1067
|
+
this.ctor = new Map(); // key -> own prefix
|
|
1068
|
+
this.ctorJoin = new Map(); // key -> join rule of its own prefix
|
|
1069
|
+
this.edges = new Map(); // target key -> [{ mounterKey, prefix }]
|
|
1070
|
+
this.edgeSeen = new Set();
|
|
1071
|
+
this.evidence = new Set(); // keys proven to be router values
|
|
1072
|
+
this.sites = new Map(); // `${file}:${callStart}` -> { key, verb, path? }
|
|
1073
|
+
this.queue = [];
|
|
1074
|
+
this.queued = new Set();
|
|
1075
|
+
this.walked = new Set();
|
|
1076
|
+
this.bindingMemo = new Map();
|
|
1077
|
+
this.fnReturnDone = new Set();
|
|
1078
|
+
this.globalPrefixes = new Set();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Call records that can register, mount, or pass a router. */
|
|
1082
|
+
relevantCall(lang, call) {
|
|
1083
|
+
if (lang === 'go') {
|
|
1084
|
+
return GO_ROUTE_VERBS.has(call.name) || GO_MOUNT_METHODS.has(call.name) ||
|
|
1085
|
+
this.goRouterParamNames().has(call.name);
|
|
1086
|
+
}
|
|
1087
|
+
if (call.isMethod) {
|
|
1088
|
+
return JS_ROUTE_VERBS.has(call.name) || JS_MOUNT_METHODS.has(call.name) ||
|
|
1089
|
+
call.receiverIsModule === true;
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
enqueue(file) {
|
|
1095
|
+
if (!file || this.queued.has(file) || !this.index.files.has(file)) return;
|
|
1096
|
+
this.queued.add(file);
|
|
1097
|
+
this.queue.push(file);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
addEdge(targetKey, mounterKey, prefix, file = null) {
|
|
1101
|
+
if (!targetKey || targetKey === mounterKey) return;
|
|
1102
|
+
const id = `${targetKey}\0${mounterKey}\0${prefix}`;
|
|
1103
|
+
if (this.edgeSeen.has(id)) return;
|
|
1104
|
+
this.edgeSeen.add(id);
|
|
1105
|
+
const list = this.edges.get(targetKey) || [];
|
|
1106
|
+
// The framework of the mounting call decides how its prefix joins
|
|
1107
|
+
// what it mounts (fix #383).
|
|
1108
|
+
const join = file && this.joinRuleOf ? this.joinRuleOf(file) : 'path';
|
|
1109
|
+
list.push({ mounterKey: mounterKey || null, prefix: prefix || '', ...(join !== 'path' && { join }) });
|
|
1110
|
+
this.edges.set(targetKey, list);
|
|
1111
|
+
if (mounterKey && this.evidence.has(mounterKey)) this.evidence.add(targetKey);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
run() {
|
|
1115
|
+
while (this.queue.length > 0) {
|
|
1116
|
+
const file = this.queue.shift();
|
|
1117
|
+
if (this.walked.has(file)) continue;
|
|
1118
|
+
this.walked.add(file);
|
|
1119
|
+
const root = this.sess.root(file);
|
|
1120
|
+
if (!root) continue;
|
|
1121
|
+
const lang = this.sess.lang(file);
|
|
1122
|
+
const visit = node => (lang === 'go' ? this.visitGoCall(file, node) : this.visitJsCall(file, node));
|
|
1123
|
+
const calls = this.callsOf ? this.callsOf(file) : null;
|
|
1124
|
+
if (calls) {
|
|
1125
|
+
// Visit only the call nodes the call records point at (the
|
|
1126
|
+
// records carry exact spans); a full-tree walk costs a native
|
|
1127
|
+
// crossing per node.
|
|
1128
|
+
for (const call of calls) {
|
|
1129
|
+
if (call.callStart == null || call.callEnd == null || !this.relevantCall(lang, call)) continue;
|
|
1130
|
+
const node = locateSpan(root, call.callStart, call.callEnd, 'call_expression');
|
|
1131
|
+
if (!node) continue;
|
|
1132
|
+
visit(node);
|
|
1133
|
+
// `require('./routes')(app)`: the outer call has no record.
|
|
1134
|
+
if (call.name === 'require' && node.parent && node.parent.type === 'call_expression' &&
|
|
1135
|
+
field(node.parent, 'function')?.startIndex === node.startIndex) {
|
|
1136
|
+
visit(node.parent);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const stack = [root];
|
|
1142
|
+
while (stack.length > 0) {
|
|
1143
|
+
const node = stack.pop();
|
|
1144
|
+
if (node.type === 'call_expression') visit(node);
|
|
1145
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) stack.push(node.namedChild(i));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// Evidence flows along mount edges (a router mounted by a router).
|
|
1149
|
+
let changed = true;
|
|
1150
|
+
while (changed) {
|
|
1151
|
+
changed = false;
|
|
1152
|
+
for (const [target, list] of this.edges) {
|
|
1153
|
+
if (this.evidence.has(target)) continue;
|
|
1154
|
+
if (list.some(e => e.mounterKey && this.evidence.has(e.mounterKey))) {
|
|
1155
|
+
this.evidence.add(target);
|
|
1156
|
+
changed = true;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ------------------------------------------------------------------ JS
|
|
1163
|
+
|
|
1164
|
+
visitJsCall(file, node) {
|
|
1165
|
+
const fn = unwrap(field(node, 'function'));
|
|
1166
|
+
const args = namedChildren(field(node, 'arguments')).filter(a => !isComment(a));
|
|
1167
|
+
if (!fn) return;
|
|
1168
|
+
if (fn.type === 'member_expression') {
|
|
1169
|
+
const method = field(fn, 'property')?.text;
|
|
1170
|
+
const obj = field(fn, 'object');
|
|
1171
|
+
if (!method || !obj) return;
|
|
1172
|
+
if (JS_ROUTE_VERBS.has(method) && args.length >= 2) {
|
|
1173
|
+
const key = this.jsRouterKey(file, obj, 0);
|
|
1174
|
+
if (key) this.addSite(file, node, key, method, args[0]);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (method === 'use' || (method === 'route' && args.length >= 2)) {
|
|
1178
|
+
if (args.length === 0) return;
|
|
1179
|
+
const mounter = this.jsRouterKey(file, obj, 0);
|
|
1180
|
+
let prefix = '';
|
|
1181
|
+
let targets = args;
|
|
1182
|
+
if (this.jsIsPathExpr(file, args[0])) {
|
|
1183
|
+
prefix = evalString(this.sess, file, args[0]);
|
|
1184
|
+
targets = args.slice(1);
|
|
1185
|
+
} else if (method === 'route') {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
for (const t of targets) {
|
|
1189
|
+
const key = this.jsRouterKey(file, t, 0);
|
|
1190
|
+
if (key) this.addEdge(key, mounter, prefix, file);
|
|
1191
|
+
}
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (method === 'register' && args.length >= 1) {
|
|
1195
|
+
const mounter = this.jsRouterKey(file, obj, 0);
|
|
1196
|
+
if (!mounter) return;
|
|
1197
|
+
let prefix = '';
|
|
1198
|
+
const opts = args[1] ? unwrap(args[1]) : null;
|
|
1199
|
+
if (opts && opts.type === 'object') {
|
|
1200
|
+
for (const pair of namedChildren(opts)) {
|
|
1201
|
+
if (pair.type === 'pair' && field(pair, 'key')?.text === 'prefix') {
|
|
1202
|
+
prefix = evalString(this.sess, file, field(pair, 'value'));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const plugin = this.jsFunctionOf(file, args[0], 0);
|
|
1207
|
+
if (!plugin) return;
|
|
1208
|
+
const params = paramNames(this.sess.lang(plugin.file), plugin.node);
|
|
1209
|
+
if (params[0]?.name) {
|
|
1210
|
+
const key = nodeKey(plugin.file, params[0].name);
|
|
1211
|
+
this.evidence.add(key);
|
|
1212
|
+
(this.paramNames || (this.paramNames = new Set())).add(params[0].name.text);
|
|
1213
|
+
this.addEdge(key, mounter, prefix, file);
|
|
1214
|
+
this.enqueue(plugin.file);
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (method === 'setGlobalPrefix' && args.length >= 1) {
|
|
1219
|
+
this.globalPrefixes.add(evalString(this.sess, file, args[0]));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// Router passed to a function: `routes(app)`, `require('./r')(router)`.
|
|
1224
|
+
if (args.length > 0 && (fn.type === 'identifier' || fn.type === 'call_expression' ||
|
|
1225
|
+
fn.type === 'member_expression')) {
|
|
1226
|
+
this.jsPassRouterArgs(file, fn, args);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
jsIsPathExpr(file, node) {
|
|
1231
|
+
const n = unwrap(node);
|
|
1232
|
+
if (!n) return false;
|
|
1233
|
+
if (n.type === 'string' || n.type === 'template_string') return true;
|
|
1234
|
+
if (n.type === 'binary_expression') return binaryOperator(n) === '+';
|
|
1235
|
+
if (n.type === 'identifier' || n.type === 'member_expression') {
|
|
1236
|
+
const v = lookupValue(this.sess, file, n, 0);
|
|
1237
|
+
if (!v || v.kind !== 'expr') return false;
|
|
1238
|
+
const vn = unwrap(v.node);
|
|
1239
|
+
return vn && (vn.type === 'string' || vn.type === 'template_string' ||
|
|
1240
|
+
(vn.type === 'binary_expression' && binaryOperator(vn) === '+'));
|
|
1241
|
+
}
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Function node denoted by an expression (inline, local, imported, require). */
|
|
1246
|
+
jsFunctionOf(file, node, depth) {
|
|
1247
|
+
if (depth > MAX_DEPTH) return null;
|
|
1248
|
+
const n = unwrap(node);
|
|
1249
|
+
if (!n) return null;
|
|
1250
|
+
if (JS_FUNCTION_TYPES.has(n.type)) return { file, node: n };
|
|
1251
|
+
if (n.type === 'identifier') {
|
|
1252
|
+
const decl = findDecl(this.sess, file, n.text, n);
|
|
1253
|
+
if (decl) {
|
|
1254
|
+
if (decl.kind === 'function') return { file, node: decl.valueNode };
|
|
1255
|
+
if (decl.valueNode) return this.jsFunctionOf(file, decl.valueNode, depth + 1);
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const rec = this.jsImportRecord(file, n.text, depth + 1);
|
|
1259
|
+
if (rec) return this.jsRecordFunction(rec, depth + 1);
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
if (n.type === 'call_expression' && this.isRequireCall(n)) {
|
|
1263
|
+
const target = resolveJsSpecifier(this.sess, file, this.requireSpec(n));
|
|
1264
|
+
const rec = target && resolveJsDefaultExport(this.sess, target, depth + 1);
|
|
1265
|
+
return rec ? this.jsRecordFunction(rec, depth + 1) : null;
|
|
1266
|
+
}
|
|
1267
|
+
if (n.type === 'member_expression') {
|
|
1268
|
+
const obj = unwrap(field(n, 'object'));
|
|
1269
|
+
const prop = field(n, 'property')?.text;
|
|
1270
|
+
if (obj && obj.type === 'identifier' && prop) {
|
|
1271
|
+
const v = lookupValue(this.sess, file, obj, depth + 1);
|
|
1272
|
+
if (v && v.kind === 'module') {
|
|
1273
|
+
const rec = resolveJsExport(this.sess, v.file, prop, depth + 1);
|
|
1274
|
+
return rec ? this.jsRecordFunction(rec, depth + 1) : null;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
jsRecordFunction(rec, depth) {
|
|
1282
|
+
if (!rec) return null;
|
|
1283
|
+
if (rec.decl && rec.decl.kind === 'function') return { file: rec.file, node: rec.decl.valueNode };
|
|
1284
|
+
if (rec.valueNode) {
|
|
1285
|
+
const v = unwrap(rec.valueNode);
|
|
1286
|
+
if (JS_FUNCTION_TYPES.has(v.type)) return { file: rec.file, node: v };
|
|
1287
|
+
if (v.type === 'identifier' || v.type === 'call_expression') {
|
|
1288
|
+
return this.jsFunctionOf(rec.file, v, depth + 1);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
jsImportRecord(file, name, depth) {
|
|
1295
|
+
const entry = this.sess.entry(file);
|
|
1296
|
+
if (!entry) return null;
|
|
1297
|
+
const bindings = entry.importBindings || [];
|
|
1298
|
+
let binding = bindings.find(b => (b.alias || b.name) === name);
|
|
1299
|
+
let original = binding ? binding.name : name;
|
|
1300
|
+
if (!binding) {
|
|
1301
|
+
const alias = (entry.importAliases || []).find(a => a.local === name);
|
|
1302
|
+
if (alias) { original = alias.original; binding = bindings.find(b => b.name === original); }
|
|
1303
|
+
}
|
|
1304
|
+
if (!binding) return null;
|
|
1305
|
+
const target = resolveJsSpecifier(this.sess, file, binding.module);
|
|
1306
|
+
if (!target || binding.kind === 'namespace') return null;
|
|
1307
|
+
return jsImportedRecord(this.sess, target, binding, original, depth + 1);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
isRequireCall(n) {
|
|
1311
|
+
const f = field(n, 'function');
|
|
1312
|
+
return f && f.type === 'identifier' && f.text === 'require';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
requireSpec(n) {
|
|
1316
|
+
const arg = namedChildren(field(n, 'arguments'))[0];
|
|
1317
|
+
return arg && arg.type === 'string' ? readLiteral(arg, () => '') : null;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** Names a file uses as routers (factory results, route/mount
|
|
1321
|
+
* receivers) - the only identifiers worth resolving as router args. */
|
|
1322
|
+
jsRouterNames(file) {
|
|
1323
|
+
if (!this._jsRouterNames) this._jsRouterNames = new Map();
|
|
1324
|
+
let names = this._jsRouterNames.get(file);
|
|
1325
|
+
if (names) return names;
|
|
1326
|
+
names = new Set();
|
|
1327
|
+
for (const call of (this.callsOf ? this.callsOf(file) : null) || []) {
|
|
1328
|
+
if (call.isMethod && call.receiver &&
|
|
1329
|
+
(JS_ROUTE_VERBS.has(call.name) || JS_MOUNT_METHODS.has(call.name))) names.add(call.receiver);
|
|
1330
|
+
if (call.assignedTo && (JS_FACTORY_CALLS.has(call.name) || JS_FACTORY_CTORS.has(call.name) ||
|
|
1331
|
+
call.name === 'basePath')) names.add(call.assignedTo);
|
|
1332
|
+
}
|
|
1333
|
+
this._jsRouterNames.set(file, names);
|
|
1334
|
+
return names;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
jsPassRouterArgs(file, fnNode, args) {
|
|
1338
|
+
const routerArgs = [];
|
|
1339
|
+
const names = this.callsOf ? this.jsRouterNames(file) : null;
|
|
1340
|
+
args.forEach((a, i) => {
|
|
1341
|
+
const n = unwrap(a);
|
|
1342
|
+
if (!n || (n.type !== 'identifier' && n.type !== 'member_expression')) return;
|
|
1343
|
+
if (names && n.type === 'identifier' && !names.has(n.text) && !this.isEvidenceParamName(file, n)) return;
|
|
1344
|
+
const key = this.jsRouterKey(file, n, 0, true);
|
|
1345
|
+
if (key && this.evidence.has(key)) routerArgs.push([i, key]);
|
|
1346
|
+
});
|
|
1347
|
+
if (routerArgs.length === 0) return;
|
|
1348
|
+
const fn = this.jsFunctionOf(file, fnNode, 0);
|
|
1349
|
+
if (!fn) return;
|
|
1350
|
+
const params = paramNames(this.sess.lang(fn.file), fn.node);
|
|
1351
|
+
for (const [i, key] of routerArgs) {
|
|
1352
|
+
const p = params[i];
|
|
1353
|
+
if (!p || !p.name) continue;
|
|
1354
|
+
const pk = nodeKey(fn.file, p.name);
|
|
1355
|
+
this.evidence.add(pk);
|
|
1356
|
+
(this.paramNames || (this.paramNames = new Set())).add(p.name.text);
|
|
1357
|
+
this.addEdge(pk, key, '', file);
|
|
1358
|
+
this.enqueue(fn.file);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** A parameter already proven to receive a router (passed on). */
|
|
1363
|
+
isEvidenceParamName(file, n) {
|
|
1364
|
+
if (!this.paramNames || !this.paramNames.has(n.text)) return false;
|
|
1365
|
+
const decl = findDecl(this.sess, file, n.text, n);
|
|
1366
|
+
return !!decl && decl.kind === 'param' && this.evidence.has(nodeKey(file, decl.nameNode));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Router value key of an expression. `lookupOnly` skips creating keys for
|
|
1371
|
+
* values with no router evidence (argument passing).
|
|
1372
|
+
*/
|
|
1373
|
+
jsRouterKey(file, node, depth, lookupOnly = false) {
|
|
1374
|
+
if (depth > MAX_DEPTH) return null;
|
|
1375
|
+
const n = unwrap(node);
|
|
1376
|
+
if (!n) return null;
|
|
1377
|
+
if (n.type === 'identifier' || n.type === 'this') {
|
|
1378
|
+
if (n.type === 'this') return null;
|
|
1379
|
+
const decl = findDecl(this.sess, file, n.text, n);
|
|
1380
|
+
if (decl) return this.jsBindingKey(file, decl, depth);
|
|
1381
|
+
const rec = this.jsImportRecord(file, n.text, depth + 1);
|
|
1382
|
+
if (rec) return this.jsRecordKey(rec, depth + 1);
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
if (n.type === 'call_expression' || n.type === 'new_expression') {
|
|
1386
|
+
return this.jsValueKey(file, n, nodeKey(file, n), depth);
|
|
1387
|
+
}
|
|
1388
|
+
if (n.type === 'member_expression') {
|
|
1389
|
+
const obj = unwrap(field(n, 'object'));
|
|
1390
|
+
const prop = field(n, 'property')?.text;
|
|
1391
|
+
if (obj && obj.type === 'identifier' && prop) {
|
|
1392
|
+
const v = lookupValue(this.sess, file, obj, depth + 1);
|
|
1393
|
+
if (v && v.kind === 'module') {
|
|
1394
|
+
const rec = resolveJsExport(this.sess, v.file, prop, depth + 1);
|
|
1395
|
+
return rec ? this.jsRecordKey(rec, depth + 1) : null;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
jsRecordKey(rec, depth) {
|
|
1403
|
+
if (!rec) return null;
|
|
1404
|
+
this.enqueue(rec.file);
|
|
1405
|
+
if (rec.decl) return this.jsBindingKey(rec.file, rec.decl, depth + 1);
|
|
1406
|
+
if (rec.valueNode) {
|
|
1407
|
+
const v = unwrap(rec.valueNode);
|
|
1408
|
+
return this.jsValueKey(rec.file, v, nodeKey(rec.file, v), depth + 1);
|
|
1409
|
+
}
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
jsBindingKey(file, decl, depth) {
|
|
1414
|
+
const key = nodeKey(file, decl.nameNode);
|
|
1415
|
+
if (decl.kind === 'param') {
|
|
1416
|
+
if (JS_ROUTER_TYPES.has(jsTypeName(decl.typeNode))) this.evidence.add(key);
|
|
1417
|
+
return key;
|
|
1418
|
+
}
|
|
1419
|
+
if (decl.kind !== 'const' && decl.kind !== 'var') return null;
|
|
1420
|
+
const memo = this.bindingMemo.get(key);
|
|
1421
|
+
if (memo !== undefined) return memo === IN_PROGRESS ? key : memo;
|
|
1422
|
+
this.bindingMemo.set(key, IN_PROGRESS);
|
|
1423
|
+
let result = key;
|
|
1424
|
+
if (decl.valueNode && decl.count === 1) {
|
|
1425
|
+
let v = unwrap(decl.valueNode);
|
|
1426
|
+
// `var app = module.exports = express()`: the assigned value.
|
|
1427
|
+
while (v?.type === 'assignment_expression') v = unwrap(field(v, 'right'));
|
|
1428
|
+
if (v.type === 'identifier') {
|
|
1429
|
+
result = this.jsRouterKey(file, v, depth + 1) || key;
|
|
1430
|
+
} else {
|
|
1431
|
+
result = this.jsValueKey(file, v, key, depth + 1) || key;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
this.bindingMemo.set(key, result);
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** Key of a router-producing expression; `key` names the value. */
|
|
1439
|
+
jsValueKey(file, v, key, depth) {
|
|
1440
|
+
if (!v || depth > MAX_DEPTH) return null;
|
|
1441
|
+
if (v.type === 'new_expression') {
|
|
1442
|
+
const ctor = field(v, 'constructor');
|
|
1443
|
+
const name = ctor ? ctor.text.split('.').pop() : null;
|
|
1444
|
+
if (name && JS_FACTORY_CTORS.has(name)) {
|
|
1445
|
+
this.evidence.add(key);
|
|
1446
|
+
// koa-router: new Router({ prefix: '/api' })
|
|
1447
|
+
const opts = unwrap(namedChildren(field(v, 'arguments'))[0]);
|
|
1448
|
+
if (opts && opts.type === 'object') {
|
|
1449
|
+
for (const pair of namedChildren(opts)) {
|
|
1450
|
+
if (pair.type === 'pair' && field(pair, 'key')?.text === 'prefix') {
|
|
1451
|
+
this.ctor.set(key, evalString(this.sess, file, field(pair, 'value')));
|
|
1452
|
+
if (this.joinRuleOf) this.ctorJoin.set(key, this.joinRuleOf(file));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return key;
|
|
1457
|
+
}
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
if (v.type !== 'call_expression') return null;
|
|
1461
|
+
const fn = unwrap(field(v, 'function'));
|
|
1462
|
+
const args = namedChildren(field(v, 'arguments')).filter(a => !isComment(a));
|
|
1463
|
+
if (!fn) return null;
|
|
1464
|
+
if (fn.type === 'identifier') {
|
|
1465
|
+
if (JS_FACTORY_CALLS.has(fn.text)) { this.evidence.add(key); return key; }
|
|
1466
|
+
if (fn.text === 'require') {
|
|
1467
|
+
const target = resolveJsSpecifier(this.sess, file, this.requireSpec(v));
|
|
1468
|
+
const rec = target && resolveJsDefaultExport(this.sess, target, depth + 1);
|
|
1469
|
+
return rec ? this.jsRecordKey(rec, depth + 1) : null;
|
|
1470
|
+
}
|
|
1471
|
+
return this.jsFunctionResultKey(file, fn, depth);
|
|
1472
|
+
}
|
|
1473
|
+
if (fn.type === 'member_expression') {
|
|
1474
|
+
const method = field(fn, 'property')?.text;
|
|
1475
|
+
const obj = field(fn, 'object');
|
|
1476
|
+
if (method === 'Router' && obj && /^express$/.test(obj.text)) {
|
|
1477
|
+
this.evidence.add(key);
|
|
1478
|
+
return key;
|
|
1479
|
+
}
|
|
1480
|
+
if (method === 'basePath' && args.length >= 1) {
|
|
1481
|
+
const parent = this.jsRouterKey(file, obj, depth + 1);
|
|
1482
|
+
this.evidence.add(key);
|
|
1483
|
+
this.addEdge(key, parent, evalString(this.sess, file, args[0]), file);
|
|
1484
|
+
return key;
|
|
1485
|
+
}
|
|
1486
|
+
// koa-router `sub.routes()`, and chained registrations
|
|
1487
|
+
// (`app.get(...).post(...)`, `app.use(mw)`) return the router.
|
|
1488
|
+
if (method === 'routes' || method === 'use' ||
|
|
1489
|
+
(JS_ROUTE_VERBS.has(method) && args.length >= 2)) {
|
|
1490
|
+
return this.jsRouterKey(file, obj, depth + 1);
|
|
1491
|
+
}
|
|
1492
|
+
return this.jsFunctionResultKey(file, fn, depth);
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** A call to a project function that returns a router. */
|
|
1498
|
+
jsFunctionResultKey(file, fnExpr, depth) {
|
|
1499
|
+
const fn = this.jsFunctionOf(file, fnExpr, depth + 1);
|
|
1500
|
+
if (!fn) return null;
|
|
1501
|
+
const fkey = `fn:${nodeKey(fn.file, fn.node)}`;
|
|
1502
|
+
if (!this.fnReturnDone.has(fkey)) {
|
|
1503
|
+
this.fnReturnDone.add(fkey);
|
|
1504
|
+
const body = field(fn.node, 'body');
|
|
1505
|
+
const returns = [];
|
|
1506
|
+
if (body && body.type !== 'statement_block') returns.push(body);
|
|
1507
|
+
else collectReturns(body, returns, JS_FUNCTION_TYPES);
|
|
1508
|
+
for (const r of returns) {
|
|
1509
|
+
const key = this.jsRouterKey(fn.file, r, depth + 1);
|
|
1510
|
+
if (key && this.evidence.has(key)) {
|
|
1511
|
+
this.addEdge(key, fkey, '', file);
|
|
1512
|
+
this.evidence.add(fkey);
|
|
1513
|
+
this.enqueue(fn.file);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return this.evidence.has(fkey) ? fkey : null;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
addSite(file, callNode, key, verb, pathArg) {
|
|
1521
|
+
const site = { key, verb };
|
|
1522
|
+
if (pathArg) {
|
|
1523
|
+
const value = evalString(this.sess, file, pathArg);
|
|
1524
|
+
if (!hasUnresolved(value)) site.path = value;
|
|
1525
|
+
}
|
|
1526
|
+
// Chained calls share a start offset; the span identifies the call.
|
|
1527
|
+
this.sites.set(`${file}:${callNode.startIndex}:${callNode.endIndex}`, site);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// ------------------------------------------------------------------ Go
|
|
1531
|
+
|
|
1532
|
+
visitGoCall(file, node) {
|
|
1533
|
+
const fn = unwrap(field(node, 'function'));
|
|
1534
|
+
const args = namedChildren(field(node, 'arguments')).filter(a => !isComment(a));
|
|
1535
|
+
if (!fn) return;
|
|
1536
|
+
if (fn.type === 'selector_expression') {
|
|
1537
|
+
const method = field(fn, 'field')?.text;
|
|
1538
|
+
const operand = field(fn, 'operand');
|
|
1539
|
+
if (!method || !operand) return;
|
|
1540
|
+
const registers = GO_ROUTE_VERBS.has(method) || method === 'Route' || method === 'Group' ||
|
|
1541
|
+
method === 'Mount';
|
|
1542
|
+
if (registers && !this.goIsPackage(file, operand)) {
|
|
1543
|
+
if (GO_ROUTE_VERBS.has(method) && args.length >= 2) {
|
|
1544
|
+
const key = this.goRouterKey(file, operand, 0);
|
|
1545
|
+
if (key) this.addSite(file, node, key, method, args[0]);
|
|
1546
|
+
// net/http: mux.Handle("/api/", http.StripPrefix("/api", sub))
|
|
1547
|
+
if ((method === 'Handle' || method === 'HandleFunc') && key) {
|
|
1548
|
+
const h = unwrap(args[1]);
|
|
1549
|
+
if (h && h.type === 'call_expression') {
|
|
1550
|
+
const hf = unwrap(field(h, 'function'));
|
|
1551
|
+
const hargs = namedChildren(field(h, 'arguments')).filter(a => !isComment(a));
|
|
1552
|
+
if (hf && hf.type === 'selector_expression' &&
|
|
1553
|
+
field(hf, 'field')?.text === 'StripPrefix' && hargs.length >= 2) {
|
|
1554
|
+
const target = this.goRouterKey(file, hargs[1], 0);
|
|
1555
|
+
if (target) this.addEdge(target, key, evalString(this.sess, file, hargs[0]), file);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if ((method === 'Route' || method === 'Group') && args.length >= 1) {
|
|
1562
|
+
const closure = unwrap(args[args.length - 1]);
|
|
1563
|
+
if (closure && closure.type === 'func_literal') {
|
|
1564
|
+
const mounter = this.goRouterKey(file, operand, 0);
|
|
1565
|
+
const prefix = method === 'Route' && args.length >= 2
|
|
1566
|
+
? evalString(this.sess, file, args[0]) : '';
|
|
1567
|
+
const p = paramNames('go', closure)[0];
|
|
1568
|
+
if (p && p.name && mounter) {
|
|
1569
|
+
const key = nodeKey(file, p.name);
|
|
1570
|
+
this.evidence.add(key);
|
|
1571
|
+
this.addEdge(key, mounter, prefix, file);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (method === 'Mount' && args.length >= 2) {
|
|
1577
|
+
const mounter = this.goRouterKey(file, operand, 0);
|
|
1578
|
+
const target = this.goRouterKey(file, args[1], 0);
|
|
1579
|
+
if (mounter && target) {
|
|
1580
|
+
this.addEdge(target, mounter, evalString(this.sess, file, args[0]), file);
|
|
1581
|
+
}
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
// Router passed to a function parameter: `api.Register(v1)`.
|
|
1587
|
+
if (args.length > 0 && (fn.type === 'identifier' || fn.type === 'selector_expression')) {
|
|
1588
|
+
const calleeName = fn.type === 'identifier' ? fn.text : field(fn, 'field')?.text;
|
|
1589
|
+
if (calleeName && this.goRouterParamNames().has(calleeName)) this.goPassRouterArgs(file, fn, args);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
goIsPackage(file, operand) {
|
|
1594
|
+
const n = unwrap(operand);
|
|
1595
|
+
if (!n || n.type !== 'identifier') return false;
|
|
1596
|
+
const entry = this.sess.entry(file);
|
|
1597
|
+
if (!(entry?.importBindings || []).some(b => b.name === n.text)) return false;
|
|
1598
|
+
return !findDecl(this.sess, file, n.text, n);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** Go function/method names with a router-typed parameter (from the
|
|
1602
|
+
* index's structured parameters; no parse). */
|
|
1603
|
+
goRouterParamNames() {
|
|
1604
|
+
if (this._goRouterParamNames) return this._goRouterParamNames;
|
|
1605
|
+
const names = new Set();
|
|
1606
|
+
for (const [name, defs] of this.index.symbols) {
|
|
1607
|
+
for (const d of defs) {
|
|
1608
|
+
if (!d.paramsStructured || this.sess.lang(d.file) !== 'go') continue;
|
|
1609
|
+
if (d.paramsStructured.some(p => isGoRouterTypeText(p.type))) { names.add(name); break; }
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
this._goRouterParamNames = names;
|
|
1613
|
+
return names;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
goPassRouterArgs(file, fnNode, args) {
|
|
1617
|
+
// Only arguments bound to a router-typed parameter are routers the
|
|
1618
|
+
// callee registers on (Go parameters are typed; this keeps the scan
|
|
1619
|
+
// to one memoized callee lookup per call site).
|
|
1620
|
+
if (!args.some(a => { const n = unwrap(a); return n && (n.type === 'identifier' ||
|
|
1621
|
+
n.type === 'call_expression' || n.type === 'unary_expression'); })) return;
|
|
1622
|
+
const fn = this.goFunctionOf(file, fnNode);
|
|
1623
|
+
if (!fn) return;
|
|
1624
|
+
const params = paramNames('go', fn.node);
|
|
1625
|
+
args.forEach((a, i) => {
|
|
1626
|
+
const p = params[i];
|
|
1627
|
+
if (!p || !p.name || !isGoRouterType(p.type)) return;
|
|
1628
|
+
const key = this.goRouterKey(file, a, 0);
|
|
1629
|
+
if (!key) return;
|
|
1630
|
+
const pk = nodeKey(fn.file, p.name);
|
|
1631
|
+
this.evidence.add(pk);
|
|
1632
|
+
this.addEdge(pk, key, '', file);
|
|
1633
|
+
this.enqueue(fn.file);
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/** Project function/method declaration a Go call expression denotes. */
|
|
1638
|
+
goFunctionOf(file, fnNode) {
|
|
1639
|
+
const n = unwrap(fnNode);
|
|
1640
|
+
if (!n) return null;
|
|
1641
|
+
if (n.type === 'func_literal') return { file, node: n };
|
|
1642
|
+
if (n.type === 'identifier' && findDecl(this.sess, file, n.text, n)) {
|
|
1643
|
+
return this.goFunctionOfUncached(file, n);
|
|
1644
|
+
}
|
|
1645
|
+
const memoKey = `gofn\0${file}\0${n.text}`;
|
|
1646
|
+
if (this.bindingMemo.has(memoKey)) return this.bindingMemo.get(memoKey);
|
|
1647
|
+
const result = this.goFunctionOfUncached(file, n);
|
|
1648
|
+
this.bindingMemo.set(memoKey, result);
|
|
1649
|
+
return result;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
goFunctionOfUncached(file, n) {
|
|
1653
|
+
let name = null;
|
|
1654
|
+
let dirs = null;
|
|
1655
|
+
let methodsOnly = false;
|
|
1656
|
+
if (n.type === 'func_literal') return { file, node: n };
|
|
1657
|
+
if (n.type === 'identifier') {
|
|
1658
|
+
const decl = findDecl(this.sess, file, n.text, n);
|
|
1659
|
+
if (decl && decl.kind === 'function') return { file, node: decl.valueNode };
|
|
1660
|
+
if (decl && decl.valueNode && unwrap(decl.valueNode).type === 'func_literal') {
|
|
1661
|
+
return { file, node: unwrap(decl.valueNode) };
|
|
1662
|
+
}
|
|
1663
|
+
if (decl) return null;
|
|
1664
|
+
name = n.text;
|
|
1665
|
+
dirs = [path.dirname(file)];
|
|
1666
|
+
} else if (n.type === 'selector_expression') {
|
|
1667
|
+
const operand = unwrap(field(n, 'operand'));
|
|
1668
|
+
name = field(n, 'field')?.text;
|
|
1669
|
+
if (this.goIsPackage(file, operand)) {
|
|
1670
|
+
const v = lookupImport(this.sess, file, operand.text, 0);
|
|
1671
|
+
if (!v || v.kind !== 'module') return null;
|
|
1672
|
+
dirs = [v.dir];
|
|
1673
|
+
} else {
|
|
1674
|
+
methodsOnly = true;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (!name) return null;
|
|
1678
|
+
const defs = (this.index.symbols.get(name) || []).filter(d => {
|
|
1679
|
+
if (this.sess.lang(d.file) !== 'go') return false;
|
|
1680
|
+
if (methodsOnly) return !!(d.isMethod || d.type === 'method' || d.receiver);
|
|
1681
|
+
if (d.isMethod || d.type === 'method' || d.receiver) return false;
|
|
1682
|
+
return dirs.includes(path.dirname(d.file));
|
|
1683
|
+
});
|
|
1684
|
+
if (defs.length !== 1) return null;
|
|
1685
|
+
const def = defs[0];
|
|
1686
|
+
const found = (goTopLevelFunctions(this.sess, def.file).get(name) || []).find(node =>
|
|
1687
|
+
node.startPosition.row + 1 <= def.startLine && node.endPosition.row + 1 >= def.startLine);
|
|
1688
|
+
return found ? { file: def.file, node: found } : null;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
goRouterKey(file, node, depth) {
|
|
1692
|
+
if (depth > MAX_DEPTH) return null;
|
|
1693
|
+
const n = unwrap(node);
|
|
1694
|
+
if (!n) return null;
|
|
1695
|
+
if (n.type === 'unary_expression') return this.goRouterKey(file, n.namedChild(0), depth + 1);
|
|
1696
|
+
if (n.type === 'identifier') {
|
|
1697
|
+
const decl = findDecl(this.sess, file, n.text, n);
|
|
1698
|
+
if (decl) return this.goBindingKey(file, decl, depth);
|
|
1699
|
+
// Package-level router variable in a sibling file.
|
|
1700
|
+
for (const f of this.sess.goPackageFiles(path.dirname(file))) {
|
|
1701
|
+
if (f === file || !this.sess.text(f).includes(n.text)) continue;
|
|
1702
|
+
const root = this.sess.root(f);
|
|
1703
|
+
const d = root && scopeDecls(this.sess, f, 'go', root).get(n.text);
|
|
1704
|
+
if (d) { this.enqueue(f); return this.goBindingKey(f, d, depth + 1); }
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
if (n.type === 'call_expression') return this.goValueKey(file, n, nodeKey(file, n), depth);
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
goBindingKey(file, decl, depth) {
|
|
1713
|
+
const key = nodeKey(file, decl.nameNode);
|
|
1714
|
+
if (decl.kind === 'param' || decl.kind === 'receiver') {
|
|
1715
|
+
if (isGoRouterType(decl.typeNode)) this.evidence.add(key);
|
|
1716
|
+
return key;
|
|
1717
|
+
}
|
|
1718
|
+
if (decl.kind !== 'var') return null;
|
|
1719
|
+
const memo = this.bindingMemo.get(key);
|
|
1720
|
+
if (memo !== undefined) return memo === IN_PROGRESS ? key : memo;
|
|
1721
|
+
this.bindingMemo.set(key, IN_PROGRESS);
|
|
1722
|
+
let result = key;
|
|
1723
|
+
if (isGoRouterType(decl.typeNode)) this.evidence.add(key);
|
|
1724
|
+
if (decl.valueNode && decl.count === 1) {
|
|
1725
|
+
const v = unwrap(decl.valueNode);
|
|
1726
|
+
if (v.type === 'identifier') result = this.goRouterKey(file, v, depth + 1) || key;
|
|
1727
|
+
else result = this.goValueKey(file, v, key, depth + 1) || key;
|
|
1728
|
+
}
|
|
1729
|
+
this.bindingMemo.set(key, result);
|
|
1730
|
+
return result;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
goValueKey(file, v, key, depth) {
|
|
1734
|
+
if (!v || depth > MAX_DEPTH) return null;
|
|
1735
|
+
if (v.type === 'unary_expression') return this.goValueKey(file, v.namedChild(0), key, depth + 1);
|
|
1736
|
+
if (v.type !== 'call_expression') return null;
|
|
1737
|
+
const fn = unwrap(field(v, 'function'));
|
|
1738
|
+
const args = namedChildren(field(v, 'arguments')).filter(a => !isComment(a));
|
|
1739
|
+
if (!fn) return null;
|
|
1740
|
+
if (fn.type === 'selector_expression') {
|
|
1741
|
+
const operand = field(fn, 'operand');
|
|
1742
|
+
const method = field(fn, 'field')?.text;
|
|
1743
|
+
if (this.goIsPackage(file, operand)) {
|
|
1744
|
+
if (GO_FACTORY_CALLS.has(`${unwrap(operand).text}.${method}`)) {
|
|
1745
|
+
this.evidence.add(key);
|
|
1746
|
+
return key;
|
|
1747
|
+
}
|
|
1748
|
+
return this.goFunctionResultKey(file, fn, depth);
|
|
1749
|
+
}
|
|
1750
|
+
// gin/echo/fiber: r.Group("/v1", mw...) - a string-first group.
|
|
1751
|
+
if (method === 'Group' && args.length >= 1 && unwrap(args[0]).type !== 'func_literal') {
|
|
1752
|
+
const parent = this.goRouterKey(file, operand, depth + 1);
|
|
1753
|
+
if (!parent) return null;
|
|
1754
|
+
this.evidence.add(key);
|
|
1755
|
+
this.addEdge(key, parent, evalString(this.sess, file, args[0]), file);
|
|
1756
|
+
return key;
|
|
1757
|
+
}
|
|
1758
|
+
// fiber: app.Route("/p", fn) returns the prefixed router.
|
|
1759
|
+
if (method === 'Route' && args.length >= 1 && unwrap(args[0]).type !== 'func_literal') {
|
|
1760
|
+
const parent = this.goRouterKey(file, operand, depth + 1);
|
|
1761
|
+
if (!parent) return null;
|
|
1762
|
+
this.evidence.add(key);
|
|
1763
|
+
this.addEdge(key, parent, evalString(this.sess, file, args[0]), file);
|
|
1764
|
+
return key;
|
|
1765
|
+
}
|
|
1766
|
+
// gorilla/mux: r.PathPrefix("/api").Subrouter()
|
|
1767
|
+
if (method === 'Subrouter') {
|
|
1768
|
+
const inner = unwrap(operand);
|
|
1769
|
+
if (inner && inner.type === 'call_expression') {
|
|
1770
|
+
const f2 = unwrap(field(inner, 'function'));
|
|
1771
|
+
const a2 = namedChildren(field(inner, 'arguments')).filter(a => !isComment(a));
|
|
1772
|
+
if (f2 && f2.type === 'selector_expression' && field(f2, 'field')?.text === 'PathPrefix' && a2.length >= 1) {
|
|
1773
|
+
const parent = this.goRouterKey(file, field(f2, 'operand'), depth + 1);
|
|
1774
|
+
if (!parent) return null;
|
|
1775
|
+
this.evidence.add(key);
|
|
1776
|
+
this.addEdge(key, parent, evalString(this.sess, file, a2[0]), file);
|
|
1777
|
+
return key;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
// chi: r.With(mw) is the same router with extra middleware.
|
|
1783
|
+
if (method === 'With') return this.goRouterKey(file, operand, depth + 1);
|
|
1784
|
+
return this.goFunctionResultKey(file, fn, depth);
|
|
1785
|
+
}
|
|
1786
|
+
if (fn.type === 'identifier') return this.goFunctionResultKey(file, fn, depth);
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
goFunctionResultKey(file, fnExpr, depth) {
|
|
1791
|
+
const fn = this.goFunctionOf(file, fnExpr);
|
|
1792
|
+
if (!fn) return null;
|
|
1793
|
+
const fkey = `fn:${nodeKey(fn.file, fn.node)}`;
|
|
1794
|
+
if (!this.fnReturnDone.has(fkey)) {
|
|
1795
|
+
this.fnReturnDone.add(fkey);
|
|
1796
|
+
const returns = [];
|
|
1797
|
+
collectReturns(field(fn.node, 'body'), returns, GO_FUNCTION_TYPES);
|
|
1798
|
+
for (const r of returns) {
|
|
1799
|
+
const key = this.goRouterKey(fn.file, r, depth + 1);
|
|
1800
|
+
if (key && this.evidence.has(key)) {
|
|
1801
|
+
this.addEdge(key, fkey, '', file);
|
|
1802
|
+
this.evidence.add(fkey);
|
|
1803
|
+
this.enqueue(fn.file);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return this.evidence.has(fkey) ? fkey : null;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const GO_ROUTER_TYPE_NAMES = new Set([
|
|
1812
|
+
'Router', 'Mux', 'Routes', 'Engine', 'RouterGroup', 'IRouter', 'IRoutes', 'Echo', 'Group',
|
|
1813
|
+
'App', 'ServeMux',
|
|
1814
|
+
]);
|
|
1815
|
+
|
|
1816
|
+
/** A router-typed Go parameter: a known framework type, or (inside the
|
|
1817
|
+
* framework's own package) the same type name unqualified. */
|
|
1818
|
+
function isGoRouterType(typeNode) {
|
|
1819
|
+
if (!typeNode) return false;
|
|
1820
|
+
const qualified = goTypeName(typeNode);
|
|
1821
|
+
if (qualified) return GO_ROUTER_TYPES.has(qualified);
|
|
1822
|
+
let t = typeNode;
|
|
1823
|
+
while (t && (t.type === 'pointer_type' || t.type === 'parenthesized_type')) t = t.namedChild(0);
|
|
1824
|
+
return !!t && t.type === 'type_identifier' && GO_ROUTER_TYPE_NAMES.has(t.text);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function isGoRouterTypeText(text) {
|
|
1828
|
+
if (!text) return false;
|
|
1829
|
+
const t = String(text).replace(/^[*(\s]+|[)\s]+$/g, '');
|
|
1830
|
+
return GO_ROUTER_TYPES.has(t) || GO_ROUTER_TYPE_NAMES.has(t);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/** Top-level function/method declarations of a Go file by name. */
|
|
1834
|
+
function goTopLevelFunctions(sess, file) {
|
|
1835
|
+
return sess.once(`gofuncs\0${file}`, new Map(), () => {
|
|
1836
|
+
const map = new Map();
|
|
1837
|
+
const root = sess.root(file);
|
|
1838
|
+
for (const node of namedChildren(root)) {
|
|
1839
|
+
if (node.type !== 'function_declaration' && node.type !== 'method_declaration') continue;
|
|
1840
|
+
const name = field(node, 'name')?.text;
|
|
1841
|
+
if (!name) continue;
|
|
1842
|
+
if (!map.has(name)) map.set(name, []);
|
|
1843
|
+
map.get(name).push(node);
|
|
1844
|
+
}
|
|
1845
|
+
return map;
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
const JS_MOUNT_METHODS = new Set(['use', 'route', 'register', 'setGlobalPrefix', 'basePath']);
|
|
1850
|
+
const GO_MOUNT_METHODS = new Set(['Group', 'Route', 'Mount', 'Handle', 'HandleFunc']);
|
|
1851
|
+
|
|
1852
|
+
/** The node of `type` spanning exactly [start, end). */
|
|
1853
|
+
function locateSpan(root, start, end, type) {
|
|
1854
|
+
let node = root.descendantForIndex(start, Math.max(start, end - 1));
|
|
1855
|
+
while (node && !(node.type === type && node.startIndex === start && node.endIndex === end)) {
|
|
1856
|
+
if (node.startIndex < start || node.endIndex > end) {
|
|
1857
|
+
// Climbed past the span without finding the exact node.
|
|
1858
|
+
if (node.type === type && node.startIndex <= start && node.endIndex >= end) break;
|
|
1859
|
+
}
|
|
1860
|
+
node = node.parent;
|
|
1861
|
+
}
|
|
1862
|
+
return node && node.type === type && node.startIndex === start && node.endIndex === end ? node : null;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/** Returned expressions of a function body, excluding nested functions. */
|
|
1866
|
+
function collectReturns(body, out, fnTypes) {
|
|
1867
|
+
if (!body) return;
|
|
1868
|
+
const stack = [body];
|
|
1869
|
+
while (stack.length > 0) {
|
|
1870
|
+
const node = stack.pop();
|
|
1871
|
+
if (node !== body && fnTypes.has(node.type)) continue;
|
|
1872
|
+
if (node.type === 'return_statement') {
|
|
1873
|
+
const values = namedChildren(node).filter(c => !isComment(c));
|
|
1874
|
+
const v = values.length === 1 && values[0].type === 'expression_list'
|
|
1875
|
+
? namedChildren(values[0]) : values;
|
|
1876
|
+
if (v.length === 1) out.push(v[0]);
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
for (let i = 0; i < node.namedChildCount; i++) stack.push(node.namedChild(i));
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* Build the router graph for JS/TS and Go files that carry mount calls.
|
|
1885
|
+
* `seedFiles` are the files whose call cache shows a mount/group call; files
|
|
1886
|
+
* reached through resolution are walked too.
|
|
1887
|
+
*/
|
|
1888
|
+
function buildRouteGraph(index, sess, seedFiles, callsOf = null, goRouterParamNames = null, joinRuleOf = null) {
|
|
1889
|
+
const graph = new RouteGraph(index, sess);
|
|
1890
|
+
graph.callsOf = callsOf;
|
|
1891
|
+
graph.joinRuleOf = joinRuleOf;
|
|
1892
|
+
if (goRouterParamNames) graph._goRouterParamNames = goRouterParamNames;
|
|
1893
|
+
for (const f of [...seedFiles].sort(codeUnitCompare)) graph.enqueue(f);
|
|
1894
|
+
graph.run();
|
|
1895
|
+
return graph;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
module.exports = {
|
|
1899
|
+
AstSession,
|
|
1900
|
+
buildRouteGraph,
|
|
1901
|
+
locateSpan,
|
|
1902
|
+
evalString,
|
|
1903
|
+
hasUnresolved,
|
|
1904
|
+
unresolved,
|
|
1905
|
+
lookupValue,
|
|
1906
|
+
lookupImport,
|
|
1907
|
+
resolveJsExport,
|
|
1908
|
+
resolveJsDefaultExport,
|
|
1909
|
+
resolveJvmClass,
|
|
1910
|
+
findClassDecls,
|
|
1911
|
+
findDecl,
|
|
1912
|
+
scopeDecls,
|
|
1913
|
+
paramNames,
|
|
1914
|
+
readLiteral,
|
|
1915
|
+
unwrap,
|
|
1916
|
+
field,
|
|
1917
|
+
namedChildren,
|
|
1918
|
+
nodeKey,
|
|
1919
|
+
isStringLiteral,
|
|
1920
|
+
GO_ROUTER_TYPES,
|
|
1921
|
+
isGoRouterTypeText,
|
|
1922
|
+
};
|