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
package/languages/javascript.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
const { referenceScope, scopeFields } = require('./lexical-scope');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -19,6 +20,7 @@ const {
|
|
|
19
20
|
buildTypeAnnotations,
|
|
20
21
|
visitNameNodes,
|
|
21
22
|
sameNode,
|
|
23
|
+
parseErrorRegions,
|
|
22
24
|
} = require('./utils');
|
|
23
25
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
24
26
|
|
|
@@ -65,6 +67,23 @@ function aliasBaseTypeName(typeNode) {
|
|
|
65
67
|
return null;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/** Whether a call's parent, through parentheses, is of `parentType` (fix #402). */
|
|
71
|
+
function directlyAwaited(node, awaitType) {
|
|
72
|
+
let parent = node.parent;
|
|
73
|
+
while (parent?.type === 'parenthesized_expression') parent = parent.parent;
|
|
74
|
+
return parent?.type === awaitType;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Whether a function/arrow node carries the `async` keyword token (fix #402). */
|
|
78
|
+
function hasAsyncToken(node) {
|
|
79
|
+
for (let i = 0; i < (node?.childCount || 0); i++) {
|
|
80
|
+
const child = node.child(i);
|
|
81
|
+
if (child.type === 'async') return true;
|
|
82
|
+
if (child.isNamed) return false;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* Check if function is a generator
|
|
70
89
|
* @param {object} node - Function node
|
|
@@ -103,6 +122,287 @@ function getAssignmentName(leftNode) {
|
|
|
103
122
|
return null;
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
// Names of the global object itself: a property assigned on one of them
|
|
126
|
+
// (unshadowed) is a global binding that bare names resolve to.
|
|
127
|
+
const JS_GLOBAL_OBJECT_NAMES = new Set(['globalThis', 'window', 'self', 'global']);
|
|
128
|
+
|
|
129
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
130
|
+
'function_declaration', 'function_expression', 'function', 'arrow_function',
|
|
131
|
+
'method_definition', 'generator_function', 'generator_function_declaration',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
function unwrapValueExpression(node) {
|
|
135
|
+
while (node && (node.type === 'parenthesized_expression' || node.type === 'as_expression' ||
|
|
136
|
+
node.type === 'non_null_expression' || node.type === 'satisfies_expression' ||
|
|
137
|
+
node.type === 'type_assertion')) {
|
|
138
|
+
node = node.type === 'type_assertion'
|
|
139
|
+
? node.namedChild(node.namedChildCount - 1) : node.namedChild(0);
|
|
140
|
+
}
|
|
141
|
+
return node;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Names a binding pattern declares (identifiers, shorthand, rest, defaults' targets). */
|
|
145
|
+
function collectPatternNames(pattern, out) {
|
|
146
|
+
if (!pattern) return;
|
|
147
|
+
if (pattern.type === 'required_parameter' || pattern.type === 'optional_parameter') {
|
|
148
|
+
collectPatternNames(pattern.childForFieldName('pattern') || pattern.childForFieldName('name'), out);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (pattern.type === 'identifier' || pattern.type === 'shorthand_property_identifier_pattern') {
|
|
152
|
+
out.push(pattern.text);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
|
|
156
|
+
collectPatternNames(pattern.childForFieldName('value'), out);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (pattern.type === 'assignment_pattern') {
|
|
160
|
+
collectPatternNames(pattern.childForFieldName('left') || pattern.childForFieldName('pattern'), out);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (pattern.type === 'type_annotation') return;
|
|
164
|
+
for (let i = 0; i < pattern.namedChildCount; i++) collectPatternNames(pattern.namedChild(i), out);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Module-level alias: findCallsInCode declares its own one-argument
|
|
168
|
+
// collectPatternNames for module value bindings.
|
|
169
|
+
const collectBoundPatternNames = collectPatternNames;
|
|
170
|
+
|
|
171
|
+
// Function kinds whose parameters bind in their own range (fix #397).
|
|
172
|
+
const JS_SHADOW_FUNCTIONS = new Set(['arrow_function', 'function_expression', 'function_declaration',
|
|
173
|
+
'function', 'method_definition', 'generator_function', 'generator_function_declaration']);
|
|
174
|
+
|
|
175
|
+
/** A declarator value that is a require()/import() (through await, parens, `.member`). */
|
|
176
|
+
function isImportInitializerNode(value) {
|
|
177
|
+
let v = value;
|
|
178
|
+
for (;;) {
|
|
179
|
+
if (!v) return false;
|
|
180
|
+
if (v.type === 'await_expression' || v.type === 'parenthesized_expression') {
|
|
181
|
+
v = v.namedChild(0);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (v.type === 'member_expression' || v.type === 'subscript_expression') {
|
|
185
|
+
v = v.childForFieldName('object');
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (v.type !== 'call_expression') return false;
|
|
191
|
+
const fn = v.childForFieldName('function');
|
|
192
|
+
return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Record the bindings one statement introduces into `table` (first wins). */
|
|
196
|
+
function collectStatementBindings(stmt, table) {
|
|
197
|
+
const add = (name, binding) => { if (name && !table.has(name)) table.set(name, binding); };
|
|
198
|
+
if (stmt.type === 'export_statement') {
|
|
199
|
+
const declaration = stmt.childForFieldName('declaration');
|
|
200
|
+
if (declaration) collectStatementBindings(declaration, table);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') {
|
|
204
|
+
for (let i = 0; i < stmt.namedChildCount; i++) {
|
|
205
|
+
const declarator = stmt.namedChild(i);
|
|
206
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
207
|
+
const target = declarator.childForFieldName('name');
|
|
208
|
+
const start = stmt.startIndex;
|
|
209
|
+
const line = stmt.startPosition.row + 1;
|
|
210
|
+
if (target?.type === 'identifier') {
|
|
211
|
+
add(target.text, { kind: 'declarator', value: declarator.childForFieldName('value'), declarator,
|
|
212
|
+
start, line });
|
|
213
|
+
} else {
|
|
214
|
+
const names = [];
|
|
215
|
+
collectPatternNames(target, names);
|
|
216
|
+
for (const name of names) add(name, { kind: 'pattern', declarator, start, line });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (stmt.type === 'function_declaration' || stmt.type === 'generator_function_declaration' ||
|
|
222
|
+
stmt.type === 'class_declaration') {
|
|
223
|
+
add(stmt.childForFieldName('name')?.text,
|
|
224
|
+
{ kind: stmt.type === 'class_declaration' ? 'class' : 'function', line: stmt.startPosition.row + 1 });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (stmt.type === 'import_statement') {
|
|
228
|
+
for (const clause of stmt.namedChildren) {
|
|
229
|
+
if (clause.type !== 'import_clause') continue;
|
|
230
|
+
for (const part of clause.namedChildren) {
|
|
231
|
+
if (part.type === 'identifier') add(part.text, { kind: 'import' });
|
|
232
|
+
else if (part.type === 'namespace_import') {
|
|
233
|
+
for (const child of part.namedChildren) {
|
|
234
|
+
if (child.type === 'identifier') add(child.text, { kind: 'import' });
|
|
235
|
+
}
|
|
236
|
+
} else if (part.type === 'named_imports') {
|
|
237
|
+
for (const spec of part.namedChildren) {
|
|
238
|
+
const local = spec.childForFieldName('alias') || spec.childForFieldName('name');
|
|
239
|
+
if (local) add(local.text, { kind: 'import' });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Statement-list binding tables per tree, by node id: one scan per list
|
|
248
|
+
// however many member assignments look names up in it.
|
|
249
|
+
const statementListTables = new WeakMap();
|
|
250
|
+
|
|
251
|
+
function statementListBindings(listNode) {
|
|
252
|
+
let byId = statementListTables.get(listNode.tree);
|
|
253
|
+
if (!byId) {
|
|
254
|
+
byId = new Map();
|
|
255
|
+
statementListTables.set(listNode.tree, byId);
|
|
256
|
+
}
|
|
257
|
+
let table = byId.get(listNode.id);
|
|
258
|
+
if (!table) {
|
|
259
|
+
table = new Map();
|
|
260
|
+
for (let i = 0; i < listNode.namedChildCount; i++) {
|
|
261
|
+
collectStatementBindings(listNode.namedChild(i), table);
|
|
262
|
+
}
|
|
263
|
+
byId.set(listNode.id, table);
|
|
264
|
+
}
|
|
265
|
+
return table;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The declaration of `name` visible at `refNode`: statement-list bindings
|
|
270
|
+
* (hoisted declarations anywhere in the list), loop and catch bindings,
|
|
271
|
+
* parameters and a function expression's own name; null when free.
|
|
272
|
+
*/
|
|
273
|
+
function declarationVisibleAt(refNode, name) {
|
|
274
|
+
for (let p = refNode.parent; p; p = p.parent) {
|
|
275
|
+
if (p.type === 'statement_block' || p.type === 'program' || p.type === 'class_static_block') {
|
|
276
|
+
const binding = statementListBindings(p).get(name);
|
|
277
|
+
if (binding) return binding;
|
|
278
|
+
} else if (p.type === 'for_statement' || p.type === 'for_in_statement') {
|
|
279
|
+
const head = p.childForFieldName('initializer') || p.childForFieldName('left');
|
|
280
|
+
if (head) {
|
|
281
|
+
const names = [];
|
|
282
|
+
if (head.type === 'lexical_declaration' || head.type === 'variable_declaration') {
|
|
283
|
+
const table = new Map();
|
|
284
|
+
collectStatementBindings(head, table);
|
|
285
|
+
if (table.has(name)) return { kind: 'loop' };
|
|
286
|
+
} else {
|
|
287
|
+
collectPatternNames(head, names);
|
|
288
|
+
if (names.includes(name)) return { kind: 'loop' };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else if (p.type === 'catch_clause') {
|
|
292
|
+
const names = [];
|
|
293
|
+
collectPatternNames(p.childForFieldName('parameter'), names);
|
|
294
|
+
if (names.includes(name)) return { kind: 'param' };
|
|
295
|
+
} else if (FUNCTION_NODE_TYPES.has(p.type)) {
|
|
296
|
+
const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
|
|
297
|
+
if (params) {
|
|
298
|
+
const names = [];
|
|
299
|
+
collectPatternNames(params, names);
|
|
300
|
+
if (names.includes(name)) return { kind: 'param' };
|
|
301
|
+
}
|
|
302
|
+
if ((p.type === 'function_expression' || p.type === 'function' ||
|
|
303
|
+
p.type === 'generator_function') && p.childForFieldName('name')?.text === name) {
|
|
304
|
+
return { kind: 'function' };
|
|
305
|
+
}
|
|
306
|
+
} else if (p.type === 'class' && p.childForFieldName('name')?.text === name) {
|
|
307
|
+
return { kind: 'class' };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A value that is a fresh or module object, never the global object. */
|
|
314
|
+
function provablyNonGlobalValue(valueNode) {
|
|
315
|
+
let value = unwrapValueExpression(valueNode);
|
|
316
|
+
while (value?.type === 'assignment_expression') {
|
|
317
|
+
value = unwrapValueExpression(value.childForFieldName('right'));
|
|
318
|
+
}
|
|
319
|
+
if (!value) return false;
|
|
320
|
+
if (['new_expression', 'object', 'array', 'class', 'function_expression', 'function',
|
|
321
|
+
'arrow_function', 'generator_function'].includes(value.type)) return true;
|
|
322
|
+
const callee = value.type === 'call_expression' ? value.childForFieldName('function') : null;
|
|
323
|
+
return callee?.type === 'identifier' && callee.text === 'require';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether `this` at a node is an object other than the global object: a
|
|
328
|
+
* class member body, or a function installed as a method (object literal
|
|
329
|
+
* value, member assignment). A plain function's `this` can be the global
|
|
330
|
+
* object (a sloppy-mode plain call), as can module-level `this` in a script.
|
|
331
|
+
*/
|
|
332
|
+
function thisIsNonGlobalObject(thisNode) {
|
|
333
|
+
for (let p = thisNode.parent; p; p = p.parent) {
|
|
334
|
+
if (p.type === 'arrow_function') continue;
|
|
335
|
+
if (p.type === 'method_definition' || p.type === 'class_body' ||
|
|
336
|
+
p.type === 'class_static_block') return true;
|
|
337
|
+
if (p.type === 'function_expression' || p.type === 'function' ||
|
|
338
|
+
p.type === 'generator_function') {
|
|
339
|
+
const holder = p.parent;
|
|
340
|
+
if (holder?.type === 'pair' && sameNode(holder.childForFieldName('value'), p)) return true;
|
|
341
|
+
return holder?.type === 'assignment_expression' &&
|
|
342
|
+
sameNode(holder.childForFieldName('right'), p) &&
|
|
343
|
+
holder.childForFieldName('left')?.type === 'member_expression';
|
|
344
|
+
}
|
|
345
|
+
if (p.type === 'function_declaration' || p.type === 'generator_function_declaration' ||
|
|
346
|
+
p.type === 'program') return false;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* What the object of a member assignment (`obj.f = function () {}`) is for
|
|
353
|
+
* bare-name lookup: 'global' when it is the global object (the member is a
|
|
354
|
+
* global binding), 'object' when it is provably another object (the member
|
|
355
|
+
* creates no name a bare call can reach), null when it may be either (a
|
|
356
|
+
* parameter, a plain function's `this`, a value UCN does not follow).
|
|
357
|
+
*/
|
|
358
|
+
function classifyAssignedObject(objectNode) {
|
|
359
|
+
const node = unwrapValueExpression(objectNode);
|
|
360
|
+
if (!node) return null;
|
|
361
|
+
if (node.type === 'this') return thisIsNonGlobalObject(node) ? 'object' : null;
|
|
362
|
+
if (node.type === 'identifier') {
|
|
363
|
+
const declaration = declarationVisibleAt(node, node.text);
|
|
364
|
+
if (!declaration) return JS_GLOBAL_OBJECT_NAMES.has(node.text) ? 'global' : 'object';
|
|
365
|
+
if (declaration.kind === 'function' || declaration.kind === 'class' ||
|
|
366
|
+
declaration.kind === 'import') return 'object';
|
|
367
|
+
if (declaration.kind === 'declarator' && provablyNonGlobalValue(declaration.value)) return 'object';
|
|
368
|
+
// `var global = globalThis` (fix #397, handlebars-measured): a
|
|
369
|
+
// binding of the global object itself, never assigned again, names
|
|
370
|
+
// the global object.
|
|
371
|
+
if (declaration.kind === 'declarator' && declaration.declarator &&
|
|
372
|
+
globalObjectAlias(declaration.declarator, node.text)) return 'global';
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
if (node.type === 'member_expression' || node.type === 'subscript_expression') {
|
|
376
|
+
// A property value is the global object only through a global
|
|
377
|
+
// alias of the global object itself (`window.self`).
|
|
378
|
+
const property = node.type === 'member_expression'
|
|
379
|
+
? node.childForFieldName('property')?.text : null;
|
|
380
|
+
if (property && JS_GLOBAL_OBJECT_NAMES.has(property) &&
|
|
381
|
+
classifyAssignedObject(node.childForFieldName('object')) === 'global') return 'global';
|
|
382
|
+
return 'object';
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Whether a declarator binds the global object and nothing assigns the name
|
|
389
|
+
* again in its scope: `var global = globalThis`, `const root = window`.
|
|
390
|
+
*/
|
|
391
|
+
function globalObjectAlias(declarator, name) {
|
|
392
|
+
const value = unwrapValueExpression(declarator.childForFieldName('value'));
|
|
393
|
+
if (value?.type !== 'identifier' || !JS_GLOBAL_OBJECT_NAMES.has(value.text) ||
|
|
394
|
+
declarationVisibleAt(value, value.text)) return false;
|
|
395
|
+
const statement = declarator.parent;
|
|
396
|
+
const scope = statement?.parent;
|
|
397
|
+
if (!scope) return false;
|
|
398
|
+
if (statement.type === 'lexical_declaration' && statement.child(0)?.type === 'const') return true;
|
|
399
|
+
for (const assignment of scope.descendantsOfType(['assignment_expression', 'augmented_assignment_expression'])) {
|
|
400
|
+
const left = assignment.childForFieldName('left');
|
|
401
|
+
if (left?.type === 'identifier' && left.text === name) return false;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
106
406
|
/** True when a declaration is outside every function/class body. */
|
|
107
407
|
function isModuleScope(node) {
|
|
108
408
|
let current = node && node.parent;
|
|
@@ -174,6 +474,16 @@ function appendObjectFunctionMembers(objectNode, functions, lines, extraFields =
|
|
|
174
474
|
}
|
|
175
475
|
const name = objectPropertyName(nameNode);
|
|
176
476
|
if (!name || !fnNode) continue;
|
|
477
|
+
// `get name() {}` / `set name(v) {}` in the literal: one property
|
|
478
|
+
// accessed by reads and writes (fix #397).
|
|
479
|
+
let accessorKind = null;
|
|
480
|
+
if (prop.type === 'method_definition') {
|
|
481
|
+
for (let c = 0; c < prop.childCount; c++) {
|
|
482
|
+
const token = prop.child(c);
|
|
483
|
+
if (nameNode && token.startIndex >= nameNode.startIndex) break;
|
|
484
|
+
if (token.type === 'get' || token.type === 'set') accessorKind = token.type;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
177
487
|
|
|
178
488
|
const paramsNode = fnNode.childForFieldName('parameters');
|
|
179
489
|
const { startLine, endLine, indent } = nodeToLocation(prop, lines);
|
|
@@ -196,6 +506,16 @@ function appendObjectFunctionMembers(objectNode, functions, lines, extraFields =
|
|
|
196
506
|
modifiers,
|
|
197
507
|
memberAssigned: true,
|
|
198
508
|
registryMember: true,
|
|
509
|
+
// The literal's own line: members of one object literal share it
|
|
510
|
+
// (fix #397: `this` in a member names that object).
|
|
511
|
+
objectLiteralLine: objectNode.startPosition.row + 1,
|
|
512
|
+
...(accessorKind && { memberType: accessorKind }),
|
|
513
|
+
// An object literal member is a property of a fresh object: no
|
|
514
|
+
// bare name reaches it, except a self-named function
|
|
515
|
+
// expression's own name inside its body (fix #384).
|
|
516
|
+
assignedObject: 'object',
|
|
517
|
+
...(fnNode.type !== 'method_definition' &&
|
|
518
|
+
fnNode.childForFieldName('name')?.text === name && { selfNamed: true }),
|
|
199
519
|
...extraFields,
|
|
200
520
|
...typeAnno,
|
|
201
521
|
...(generics && { generics }),
|
|
@@ -769,8 +1089,16 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
769
1089
|
if (!isArrow && !isFnExpr && isModuleScope(node)) {
|
|
770
1090
|
const registryObject = unwrapObjectRegistry(valueNode);
|
|
771
1091
|
if (registryObject) {
|
|
1092
|
+
// The container's declared type (`const h: Handler =
|
|
1093
|
+
// {...}`) makes each member a structural contract
|
|
1094
|
+
// member of that type (fix #360: plan rename
|
|
1095
|
+
// closure over interface slots).
|
|
1096
|
+
const containerTypeNode = declarator.childForFieldName('type');
|
|
1097
|
+
const containerType = containerTypeNode
|
|
1098
|
+
? containerTypeNode.text.replace(/^:\s*/, '').trim() : null;
|
|
772
1099
|
const added = appendObjectFunctionMembers(registryObject, functions, lines, {
|
|
773
1100
|
registryContainer: nameNode.text,
|
|
1101
|
+
...(containerType && { registryContainerType: containerType }),
|
|
774
1102
|
});
|
|
775
1103
|
if (added > 0) processedRanges.add(rangeKey);
|
|
776
1104
|
}
|
|
@@ -849,6 +1177,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
849
1177
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
850
1178
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
851
1179
|
|
|
1180
|
+
const isAsync = hasAsyncToken(rightNode);
|
|
852
1181
|
functions.push({
|
|
853
1182
|
name,
|
|
854
1183
|
params: extractParams(paramsNode),
|
|
@@ -858,7 +1187,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
858
1187
|
indent,
|
|
859
1188
|
isArrow,
|
|
860
1189
|
isGenerator: isGen,
|
|
861
|
-
|
|
1190
|
+
...(isAsync && { isAsync }),
|
|
1191
|
+
modifiers: [...(isCommonJsDefault ? ['export'] : []), ...(isAsync ? ['async'] : [])],
|
|
862
1192
|
// A property-assignment def (Reply.prototype.serialize
|
|
863
1193
|
// = function, exports.h = () => ...) creates NO
|
|
864
1194
|
// lexical name — a bare call in the file can never
|
|
@@ -875,6 +1205,18 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
875
1205
|
...(leftNode.type === 'member_expression' &&
|
|
876
1206
|
leftNode.childForFieldName('object')?.type === 'identifier' &&
|
|
877
1207
|
{ assignedReceiver: leftNode.childForFieldName('object').text }),
|
|
1208
|
+
// Bare-name reachability of the member (fix #384): a
|
|
1209
|
+
// bare call resolves lexically, then globally, so it
|
|
1210
|
+
// reaches the member only when the object is the
|
|
1211
|
+
// global object, or inside a self-named function
|
|
1212
|
+
// expression's own body (ECMA-262 binds that name
|
|
1213
|
+
// there).
|
|
1214
|
+
...(leftNode.type === 'member_expression' && (() => {
|
|
1215
|
+
const kind = classifyAssignedObject(leftNode.childForFieldName('object'));
|
|
1216
|
+
return kind ? { assignedObject: kind } : {};
|
|
1217
|
+
})()),
|
|
1218
|
+
...(leftNode.type === 'member_expression' && expressionName === name &&
|
|
1219
|
+
{ selfNamed: true }),
|
|
878
1220
|
...(prototypeOwner && { className: prototypeOwner, isMethod: true }),
|
|
879
1221
|
...typeAnno,
|
|
880
1222
|
...(generics && { generics }),
|
|
@@ -920,6 +1262,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
920
1262
|
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
921
1263
|
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
922
1264
|
|
|
1265
|
+
const isAsync = hasAsyncToken(child);
|
|
923
1266
|
functions.push({
|
|
924
1267
|
name: 'default',
|
|
925
1268
|
params: extractParams(paramsNode),
|
|
@@ -929,7 +1272,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
929
1272
|
indent,
|
|
930
1273
|
isArrow: child.type === 'arrow_function',
|
|
931
1274
|
isGenerator: isGen,
|
|
932
|
-
|
|
1275
|
+
...(isAsync && { isAsync }),
|
|
1276
|
+
modifiers: isAsync ? ['export', 'async', 'default'] : ['export', 'default'],
|
|
933
1277
|
...typeAnno,
|
|
934
1278
|
...(generics && { generics }),
|
|
935
1279
|
...(docstring && { docstring })
|
|
@@ -994,7 +1338,10 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
994
1338
|
...(extendsInfo && { extends: extendsInfo }),
|
|
995
1339
|
...(implementsInfo.length > 0 && { implements: implementsInfo }),
|
|
996
1340
|
...(decorators.length > 0 && { decorators }),
|
|
997
|
-
...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs })
|
|
1341
|
+
...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs }),
|
|
1342
|
+
// A class declared in a function body is visible only there
|
|
1343
|
+
// (fix #378).
|
|
1344
|
+
...(node.type !== 'class' && lexicalOwnerRange(node)),
|
|
998
1345
|
});
|
|
999
1346
|
}
|
|
1000
1347
|
return true;
|
|
@@ -1473,6 +1820,10 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1473
1820
|
if (nameNode) {
|
|
1474
1821
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
1475
1822
|
const name = nameNode.text;
|
|
1823
|
+
// fix #390: a TS decorator is part of the field node; the
|
|
1824
|
+
// name's line is where definition edits go.
|
|
1825
|
+
const fieldNameLine = nameNode.startPosition.row + 1 !== startLine
|
|
1826
|
+
? { nameLine: nameNode.startPosition.row + 1 } : {};
|
|
1476
1827
|
const valueNode = child.childForFieldName('value');
|
|
1477
1828
|
const isArrow = valueNode && valueNode.type === 'arrow_function';
|
|
1478
1829
|
const isStatic = Array.from({ length: child.childCount }, (_, ci) => child.child(ci))
|
|
@@ -1514,6 +1865,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1514
1865
|
paramsStructured,
|
|
1515
1866
|
startLine,
|
|
1516
1867
|
endLine,
|
|
1868
|
+
...fieldNameLine,
|
|
1517
1869
|
memberType: name.startsWith('#') ? 'private' : 'field',
|
|
1518
1870
|
...(isStatic && { modifiers: ['static'] }),
|
|
1519
1871
|
isArrow: true,
|
|
@@ -1540,6 +1892,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1540
1892
|
name,
|
|
1541
1893
|
startLine,
|
|
1542
1894
|
endLine,
|
|
1895
|
+
...fieldNameLine,
|
|
1543
1896
|
memberType: name.startsWith('#') ? 'private field' : 'field',
|
|
1544
1897
|
...(isStatic && { modifiers: ['static'] }),
|
|
1545
1898
|
...(fieldType && { fieldType }),
|
|
@@ -1663,6 +2016,70 @@ function findStateObjects(code, parser) {
|
|
|
1663
2016
|
* @param {object} parser - Tree-sitter parser instance
|
|
1664
2017
|
* @returns {ParseResult}
|
|
1665
2018
|
*/
|
|
2019
|
+
/**
|
|
2020
|
+
* Module-level `const` bindings of another name (fix #389): `const Alias =
|
|
2021
|
+
* Box;`, `export const Alias = models.Box;`. A const is bound once; query
|
|
2022
|
+
* time decides whether the target is a class (then `new Alias()` makes a
|
|
2023
|
+
* Box).
|
|
2024
|
+
*/
|
|
2025
|
+
function jsModuleValueAliases(root) {
|
|
2026
|
+
const aliases = [];
|
|
2027
|
+
const mutable = [];
|
|
2028
|
+
const declared = new Map();
|
|
2029
|
+
for (let statement of root.namedChildren) {
|
|
2030
|
+
if (statement.type === 'export_statement') {
|
|
2031
|
+
statement = statement.childForFieldName('declaration') ||
|
|
2032
|
+
statement.namedChildren.find(child => child.type === 'lexical_declaration');
|
|
2033
|
+
}
|
|
2034
|
+
if (statement?.type !== 'lexical_declaration' && statement?.type !== 'variable_declaration') continue;
|
|
2035
|
+
const isConst = statement.children.some(child => child.type === 'const');
|
|
2036
|
+
for (const declarator of statement.namedChildren) {
|
|
2037
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
2038
|
+
const nameNode = declarator.childForFieldName('name');
|
|
2039
|
+
if (nameNode?.type === 'identifier') declared.set(nameNode.text, (declared.get(nameNode.text) || 0) + 1);
|
|
2040
|
+
let value = declarator.childForFieldName('value');
|
|
2041
|
+
if (declarator.childForFieldName('type')) continue;
|
|
2042
|
+
while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
|
|
2043
|
+
if (nameNode?.type !== 'identifier' || !value) continue;
|
|
2044
|
+
if ((value.type === 'identifier' || value.type === 'member_expression') &&
|
|
2045
|
+
/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(value.text) &&
|
|
2046
|
+
value.text !== nameNode.text) {
|
|
2047
|
+
const alias = { name: nameNode.text, target: value.text,
|
|
2048
|
+
line: declarator.startPosition.row + 1 };
|
|
2049
|
+
if (isConst) aliases.push(alias);
|
|
2050
|
+
else mutable.push(alias);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (mutable.length > 0) {
|
|
2055
|
+
// A `let`/`var` alias is the class only while nothing assigns the
|
|
2056
|
+
// name again (fix #392): any write to that identifier anywhere in the
|
|
2057
|
+
// module, or a second declaration, leaves it unbound.
|
|
2058
|
+
const written = new Set();
|
|
2059
|
+
const writeTargets = (node) => {
|
|
2060
|
+
if (!node) return;
|
|
2061
|
+
if (node.type === 'identifier') { written.add(node.text); return; }
|
|
2062
|
+
if (/pattern$/.test(node.type) || node.type === 'pair_pattern' ||
|
|
2063
|
+
node.type === 'shorthand_property_identifier_pattern') {
|
|
2064
|
+
if (node.type === 'shorthand_property_identifier_pattern') written.add(node.text);
|
|
2065
|
+
for (const child of node.namedChildren) writeTargets(child);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
for (const node of root.descendantsOfType(['assignment_expression',
|
|
2069
|
+
'augmented_assignment_expression', 'update_expression', 'for_in_statement'])) {
|
|
2070
|
+
if (node.type === 'update_expression') writeTargets(node.namedChild(0));
|
|
2071
|
+
else if (node.type === 'for_in_statement') {
|
|
2072
|
+
if (!node.childForFieldName('kind')) writeTargets(node.childForFieldName('left'));
|
|
2073
|
+
} else writeTargets(node.childForFieldName('left'));
|
|
2074
|
+
}
|
|
2075
|
+
for (const alias of mutable) {
|
|
2076
|
+
if (!written.has(alias.name) && declared.get(alias.name) === 1) aliases.push(alias);
|
|
2077
|
+
}
|
|
2078
|
+
aliases.sort((a, b) => a.line - b.line);
|
|
2079
|
+
}
|
|
2080
|
+
return aliases;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1666
2083
|
function parse(code, parser) {
|
|
1667
2084
|
const tree = parseTree(parser, code);
|
|
1668
2085
|
const lines = code.split('\n');
|
|
@@ -1774,6 +2191,7 @@ function parse(code, parser) {
|
|
|
1774
2191
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
1775
2192
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
1776
2193
|
callableAliases.sort((a, b) => a.startLine - b.startLine);
|
|
2194
|
+
const moduleValueAliases = jsModuleValueAliases(tree.rootNode);
|
|
1777
2195
|
|
|
1778
2196
|
return {
|
|
1779
2197
|
language: 'javascript',
|
|
@@ -1782,7 +2200,8 @@ function parse(code, parser) {
|
|
|
1782
2200
|
classes,
|
|
1783
2201
|
stateObjects,
|
|
1784
2202
|
callableAliases,
|
|
1785
|
-
...(
|
|
2203
|
+
...(moduleValueAliases.length > 0 && { moduleValueAliases }),
|
|
2204
|
+
...(tree.rootNode.hasError && { parseRecovery: true, parseErrorRegions: parseErrorRegions(tree.rootNode) }),
|
|
1786
2205
|
imports: [], // Handled by core/imports.js
|
|
1787
2206
|
exports: [] // Handled by core/imports.js
|
|
1788
2207
|
};
|
|
@@ -1912,11 +2331,36 @@ function tsArrayElement(node) {
|
|
|
1912
2331
|
!(child.type === 'literal_type' && ['null', 'undefined'].includes(child.text)));
|
|
1913
2332
|
return present.length === 1 ? tsArrayElement(present[0]) : null;
|
|
1914
2333
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
2334
|
+
let item = null;
|
|
2335
|
+
let keyed = false;
|
|
2336
|
+
if (node.type === 'array_type') {
|
|
2337
|
+
item = node.namedChild(0);
|
|
2338
|
+
} else if (node.type === 'generic_type') {
|
|
2339
|
+
// Declared container slots (fix #359): Array<T> / ReadonlyArray<T>
|
|
2340
|
+
// index to T; Record<K, V> keys to V.
|
|
2341
|
+
const base = node.childForFieldName('name') || node.namedChild(0);
|
|
2342
|
+
const argsNode = node.childForFieldName('type_arguments') ||
|
|
2343
|
+
node.namedChildren.find(child => child.type === 'type_arguments');
|
|
2344
|
+
const args = argsNode?.namedChildren || [];
|
|
2345
|
+
if (base?.type === 'type_identifier' &&
|
|
2346
|
+
['Array', 'ReadonlyArray'].includes(base.text) && args.length === 1) {
|
|
2347
|
+
item = args[0];
|
|
2348
|
+
} else if (base?.type === 'type_identifier' && base.text === 'Record' &&
|
|
2349
|
+
args.length === 2) {
|
|
2350
|
+
item = args[1];
|
|
2351
|
+
keyed = true;
|
|
2352
|
+
}
|
|
2353
|
+
} else if (node.type === 'object_type' && node.namedChildCount === 1 &&
|
|
2354
|
+
node.namedChild(0).type === 'index_signature') {
|
|
2355
|
+
// `{ [key: string]: T }` keys to T.
|
|
2356
|
+
const signature = node.namedChild(0);
|
|
2357
|
+
const valueType = signature.childForFieldName('type');
|
|
2358
|
+
item = valueType?.type === 'type_annotation' ? valueType.namedChild(0) : valueType;
|
|
2359
|
+
keyed = true;
|
|
2360
|
+
}
|
|
1917
2361
|
if (!['type_identifier', 'nested_type_identifier', 'generic_type', 'predefined_type'].includes(item?.type)) return null;
|
|
1918
2362
|
const type = tsTypeName(item);
|
|
1919
|
-
return type ? { type, qualifier: tsTypeQualifier(item), node: item } : null;
|
|
2363
|
+
return type ? { type, qualifier: tsTypeQualifier(item), node: item, ...(keyed && { keyed: true }) } : null;
|
|
1920
2364
|
}
|
|
1921
2365
|
|
|
1922
2366
|
/**
|
|
@@ -2020,7 +2464,82 @@ function findCallsInCode(code, parser) {
|
|
|
2020
2464
|
}
|
|
2021
2465
|
return current?.type === 'identifier' ? current.text : undefined;
|
|
2022
2466
|
};
|
|
2467
|
+
// Local binding scopes by name (fix #397): every function-level
|
|
2468
|
+
// declaration, parameter, catch and loop binding with the source range it
|
|
2469
|
+
// binds in, so a reference's shadowing binding is found by range instead
|
|
2470
|
+
// of a parent walk; and the names object patterns bind.
|
|
2471
|
+
const bindingScopes = new Map(); // name -> [{ start, end, declStart, line }]
|
|
2472
|
+
const destructuredNames = new Set();
|
|
2473
|
+
const addScope = (pattern, scope, declStart, line) => {
|
|
2474
|
+
const names = [];
|
|
2475
|
+
collectBoundPatternNames(pattern, names);
|
|
2476
|
+
for (const n of names) {
|
|
2477
|
+
let list = bindingScopes.get(n);
|
|
2478
|
+
if (!list) { list = []; bindingScopes.set(n, list); }
|
|
2479
|
+
list.push({ start: scope.startIndex, end: scope.endIndex, declStart, line });
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2023
2482
|
traverseTreeCached(tree.rootNode, node => {
|
|
2483
|
+
switch (node.type) {
|
|
2484
|
+
case 'variable_declarator': {
|
|
2485
|
+
const statement = node.parent;
|
|
2486
|
+
const host = statement?.parent;
|
|
2487
|
+
if (host?.type === 'statement_block') {
|
|
2488
|
+
// A declarator initialized from require()/import() is the
|
|
2489
|
+
// module's binding reaching the scope, not a shadow (#337).
|
|
2490
|
+
if (!isImportInitializerNode(node.childForFieldName('value'))) {
|
|
2491
|
+
addScope(node.childForFieldName('name'), host, statement.startIndex,
|
|
2492
|
+
statement.startPosition.row + 1);
|
|
2493
|
+
}
|
|
2494
|
+
} else if (host?.type === 'for_statement' || host?.type === 'for_in_statement') {
|
|
2495
|
+
addScope(node.childForFieldName('name'), host, null, -1);
|
|
2496
|
+
}
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
case 'formal_parameters':
|
|
2500
|
+
if (node.parent && JS_SHADOW_FUNCTIONS.has(node.parent.type)) addScope(node, node.parent, null, -1);
|
|
2501
|
+
break;
|
|
2502
|
+
case 'arrow_function': {
|
|
2503
|
+
const single = node.childForFieldName('parameter');
|
|
2504
|
+
if (single) addScope(single, node, null, -1);
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
case 'catch_clause': {
|
|
2508
|
+
const param = node.childForFieldName('parameter');
|
|
2509
|
+
if (param) addScope(param, node, null, -1);
|
|
2510
|
+
break;
|
|
2511
|
+
}
|
|
2512
|
+
case 'for_in_statement': {
|
|
2513
|
+
const left = node.childForFieldName('left');
|
|
2514
|
+
if (left && left.type !== 'lexical_declaration' && left.type !== 'variable_declaration') {
|
|
2515
|
+
addScope(left, node, null, -1);
|
|
2516
|
+
}
|
|
2517
|
+
break;
|
|
2518
|
+
}
|
|
2519
|
+
case 'function_declaration': case 'generator_function_declaration': case 'class_declaration':
|
|
2520
|
+
if (node.parent?.type === 'statement_block') {
|
|
2521
|
+
const nameNode = node.childForFieldName('name');
|
|
2522
|
+
// Hoisted within the block: shadows at any position.
|
|
2523
|
+
if (nameNode) addScope(nameNode, node.parent, null, node.startPosition.row + 1);
|
|
2524
|
+
}
|
|
2525
|
+
break;
|
|
2526
|
+
case 'object_pattern':
|
|
2527
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2528
|
+
const prop = node.namedChild(i);
|
|
2529
|
+
if (prop.type === 'shorthand_property_identifier_pattern') destructuredNames.add(prop.text);
|
|
2530
|
+
else if (prop.type === 'object_assignment_pattern') {
|
|
2531
|
+
const left = prop.childForFieldName('left');
|
|
2532
|
+
if (left) destructuredNames.add(left.text);
|
|
2533
|
+
} else if (prop.type === 'pair_pattern') {
|
|
2534
|
+
let value = prop.childForFieldName('value');
|
|
2535
|
+
if (value?.type === 'assignment_pattern') value = value.childForFieldName('left');
|
|
2536
|
+
if (value?.type === 'identifier') destructuredNames.add(value.text);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
break;
|
|
2540
|
+
default:
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2024
2543
|
if (['variable_declarator', 'required_parameter', 'optional_parameter'].includes(node.type)) {
|
|
2025
2544
|
const name = node.childForFieldName('name') || node.childForFieldName('pattern');
|
|
2026
2545
|
if (name?.type === 'identifier' && tsArrayElement(node.childForFieldName('type'))) {
|
|
@@ -2136,6 +2655,10 @@ function findCallsInCode(code, parser) {
|
|
|
2136
2655
|
const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
|
|
2137
2656
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
2138
2657
|
const localVarTypes = new ReceiverTypeMap(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
2658
|
+
// One-hop `const` aliases of member paths (fix #381):
|
|
2659
|
+
// `const c = this.config; c.load()` receives through `this.config`.
|
|
2660
|
+
// name -> { node: the aliased member_expression, scope: declaring block }.
|
|
2661
|
+
const localFieldAliases = new Map();
|
|
2139
2662
|
const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
|
|
2140
2663
|
// Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
|
|
2141
2664
|
// params). The compiler enforces assignability for these, so reassignment
|
|
@@ -2177,6 +2700,176 @@ function findCallsInCode(code, parser) {
|
|
|
2177
2700
|
return count;
|
|
2178
2701
|
};
|
|
2179
2702
|
|
|
2703
|
+
// fix #366: request-config calls — `request(cfg, { method: 'POST',
|
|
2704
|
+
// url: '/api/users' })`, `client.post({ url: '/api/items' })` (generated
|
|
2705
|
+
// OpenAPI SDKs). Returns { url, interp, method? } for the first object
|
|
2706
|
+
// literal argument carrying a string `url`/`path` property. Whether the
|
|
2707
|
+
// callee really performs HTTP is decided at query time from the callee's
|
|
2708
|
+
// definition, never from this record.
|
|
2709
|
+
const getRequestConfig = (callNode) => {
|
|
2710
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2711
|
+
if (!argsNode) return null;
|
|
2712
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2713
|
+
const arg = argsNode.namedChild(i);
|
|
2714
|
+
if (arg.type !== 'object') continue;
|
|
2715
|
+
let url = null;
|
|
2716
|
+
let method = null;
|
|
2717
|
+
let handler = null;
|
|
2718
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2719
|
+
const prop = arg.namedChild(j);
|
|
2720
|
+
if (prop.type !== 'pair') continue;
|
|
2721
|
+
const keyNode = prop.childForFieldName('key');
|
|
2722
|
+
const valNode = prop.childForFieldName('value');
|
|
2723
|
+
if (!keyNode || !valNode) continue;
|
|
2724
|
+
const keyName = keyNode.type === 'string'
|
|
2725
|
+
? keyNode.text.replace(/^['"`]|['"`]$/g, '') : keyNode.text;
|
|
2726
|
+
if ((keyName === 'url' || keyName === 'path') && !url) {
|
|
2727
|
+
const v = _extractStringArg(valNode);
|
|
2728
|
+
if (v && typeof v.value === 'string' && v.value.length > 0) {
|
|
2729
|
+
url = { value: v.value, interp: !!v.interp, key: keyName };
|
|
2730
|
+
}
|
|
2731
|
+
} else if (keyName === 'method') {
|
|
2732
|
+
const v = _extractStringArg(valNode);
|
|
2733
|
+
if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
|
|
2734
|
+
method = v.value.toUpperCase();
|
|
2735
|
+
}
|
|
2736
|
+
} else if (keyName === 'handler') {
|
|
2737
|
+
// fix #383: `route({ method, url, handler })` names its handler.
|
|
2738
|
+
handler = handlerNameOf(valNode);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
if (url) {
|
|
2742
|
+
return { url: url.value, key: url.key, ...(url.interp && { interp: true }),
|
|
2743
|
+
...(method && { method }), ...(handler && { handler }) };
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
return null;
|
|
2747
|
+
};
|
|
2748
|
+
|
|
2749
|
+
// fix #383: the arguments of a route-shaped registration
|
|
2750
|
+
// (`app.get('/x', mw, handler)`) after its path: each argument's name,
|
|
2751
|
+
// '<anonymous>' for an inline function, the callee's name for a call
|
|
2752
|
+
// (`wrap(handler)`), null for any other expression.
|
|
2753
|
+
const ROUTE_VERB_METHODS = /^(get|post|put|delete|patch|options|head|all)$/;
|
|
2754
|
+
const handlerNameOf = (node) => {
|
|
2755
|
+
if (!node) return null;
|
|
2756
|
+
if (node.type === 'identifier') return node.text;
|
|
2757
|
+
if (node.type === 'member_expression') return node.childForFieldName('property')?.text || null;
|
|
2758
|
+
if (/function|arrow/.test(node.type)) return '<anonymous>';
|
|
2759
|
+
if (node.type === 'call_expression') {
|
|
2760
|
+
const fn = node.childForFieldName('function');
|
|
2761
|
+
if (fn?.type === 'identifier') return fn.text;
|
|
2762
|
+
if (fn?.type === 'member_expression') return fn.childForFieldName('property')?.text || null;
|
|
2763
|
+
}
|
|
2764
|
+
return null;
|
|
2765
|
+
};
|
|
2766
|
+
// The names of a registration's arguments after its path (null for an
|
|
2767
|
+
// expression with no name); the router decides which one is the handler.
|
|
2768
|
+
const getHandlerArgs = (callNode) => {
|
|
2769
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2770
|
+
if (!argsNode) return null;
|
|
2771
|
+
const out = [];
|
|
2772
|
+
let first = true;
|
|
2773
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2774
|
+
const arg = argsNode.namedChild(i);
|
|
2775
|
+
if (arg.type.endsWith('comment')) continue;
|
|
2776
|
+
if (first) { first = false; continue; }
|
|
2777
|
+
out.push(handlerNameOf(arg));
|
|
2778
|
+
}
|
|
2779
|
+
return out.length > 0 ? out : null;
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
const hasObjectKeyArg = (callNode, key) => {
|
|
2783
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2784
|
+
if (!argsNode) return false;
|
|
2785
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2786
|
+
const arg = argsNode.namedChild(i);
|
|
2787
|
+
if (arg.type !== 'object') continue;
|
|
2788
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2789
|
+
const prop = arg.namedChild(j);
|
|
2790
|
+
const keyNode = prop.type === 'pair' ? prop.childForFieldName('key') : prop;
|
|
2791
|
+
if (keyNode && keyNode.text.replace(/^['"`]|['"`]$/g, '') === key) return true;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return false;
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
const getMountArgs = (callNode) => {
|
|
2798
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2799
|
+
if (!argsNode) return null;
|
|
2800
|
+
const out = [];
|
|
2801
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2802
|
+
const arg = argsNode.namedChild(i);
|
|
2803
|
+
if (arg.type.endsWith('comment')) continue;
|
|
2804
|
+
if (arg.type === 'identifier') out.push(arg.text);
|
|
2805
|
+
else if (arg.type === 'string' || (arg.type === 'template_string' && arg.namedChildCount <= 1)) out.push('""');
|
|
2806
|
+
else if (arg.type.includes('function')) out.push('fn');
|
|
2807
|
+
else out.push('()');
|
|
2808
|
+
}
|
|
2809
|
+
return out;
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// fix #366: `x.register(plugin, { prefix: '/v1' })` (Fastify plugin
|
|
2813
|
+
// mounts). Records the prefix option (literal, or `prefixDynamic` for an
|
|
2814
|
+
// expression) and, for an inline plugin function, its span and first
|
|
2815
|
+
// parameter - the router the plugin registers routes on. Returns null
|
|
2816
|
+
// when the call carries neither.
|
|
2817
|
+
const getRegisterMount = (callNode) => {
|
|
2818
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2819
|
+
if (!argsNode) return null;
|
|
2820
|
+
const args = [];
|
|
2821
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2822
|
+
const arg = argsNode.namedChild(i);
|
|
2823
|
+
if (!arg.type.endsWith('comment')) args.push(arg);
|
|
2824
|
+
}
|
|
2825
|
+
if (args.length === 0) return null;
|
|
2826
|
+
const out = {};
|
|
2827
|
+
const opts = args[1];
|
|
2828
|
+
if (opts && opts.type === 'object') {
|
|
2829
|
+
for (let j = 0; j < opts.namedChildCount; j++) {
|
|
2830
|
+
const prop = opts.namedChild(j);
|
|
2831
|
+
const keyNode = prop.type === 'pair' ? prop.childForFieldName('key') : prop;
|
|
2832
|
+
if (!keyNode || keyNode.text.replace(/^['"`]|['"`]$/g, '') !== 'prefix') continue;
|
|
2833
|
+
const v = prop.type === 'pair' ? _extractStringArg(prop.childForFieldName('value')) : null;
|
|
2834
|
+
if (v && !v.interp && typeof v.value === 'string') out.prefix = v.value;
|
|
2835
|
+
else out.prefixDynamic = true;
|
|
2836
|
+
}
|
|
2837
|
+
} else if (opts) {
|
|
2838
|
+
out.prefixDynamic = true;
|
|
2839
|
+
}
|
|
2840
|
+
let plugin = args[0];
|
|
2841
|
+
// `register(fp(function (instance) {...}))`: a wrapper call around an
|
|
2842
|
+
// inline plugin function registers that function.
|
|
2843
|
+
if (plugin.type === 'call_expression') {
|
|
2844
|
+
const inner = plugin.childForFieldName('arguments')?.namedChild(0);
|
|
2845
|
+
if (inner && ['arrow_function', 'function_expression', 'function'].includes(inner.type)) plugin = inner;
|
|
2846
|
+
}
|
|
2847
|
+
if (['arrow_function', 'function_expression', 'function'].includes(plugin.type)) {
|
|
2848
|
+
const params = plugin.childForFieldName('parameters') || plugin.childForFieldName('parameter');
|
|
2849
|
+
let first = params && params.type === 'identifier' ? params : null;
|
|
2850
|
+
if (!first && params) {
|
|
2851
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
2852
|
+
const p = params.namedChild(i);
|
|
2853
|
+
if (p.type.endsWith('comment')) continue;
|
|
2854
|
+
const pat = p.type === 'identifier' ? p : p.childForFieldName('pattern');
|
|
2855
|
+
first = pat && pat.type === 'identifier' ? pat : null;
|
|
2856
|
+
break;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (first) {
|
|
2860
|
+
out.plugin = { start: plugin.startIndex, end: plugin.endIndex, param: first.text };
|
|
2861
|
+
}
|
|
2862
|
+
} else if (plugin.type === 'identifier') {
|
|
2863
|
+
// A named plugin: a same-file function composes from the index;
|
|
2864
|
+
// anything else is resolved by the endpoints graph.
|
|
2865
|
+
out.pluginName = plugin.text;
|
|
2866
|
+
} else if (plugin.type === 'member_expression' || plugin.type === 'call_expression') {
|
|
2867
|
+
out.pluginRef = true;
|
|
2868
|
+
}
|
|
2869
|
+
return (out.prefix != null || out.prefixDynamic || out.plugin || out.pluginRef || out.pluginName)
|
|
2870
|
+
? out : null;
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2180
2873
|
// MEDIUM-5: extract HTTP method from `fetch(url, { method: 'POST' })`
|
|
2181
2874
|
// and similar XHR/Request-init shapes. Returns the upper-cased method
|
|
2182
2875
|
// string or null. Looks at argument index `argIdx` (default 1, the
|
|
@@ -2437,8 +3130,12 @@ function findCallsInCode(code, parser) {
|
|
|
2437
3130
|
const root = object.childForFieldName('object');
|
|
2438
3131
|
const offset = object.childForFieldName('index');
|
|
2439
3132
|
if (root?.type !== 'identifier' || !arrayAnnotationNames.has(root.text) || !offset) return null;
|
|
2440
|
-
|
|
2441
|
-
|
|
3133
|
+
const binding = indexedBinding(root.text, object);
|
|
3134
|
+
const element = tsArrayElement(binding?.type);
|
|
3135
|
+
// A keyed container (Record / index signature) accepts any key; an
|
|
3136
|
+
// array index must be numeric (a string key reads a property).
|
|
3137
|
+
let numeric = offset.type === 'number' || !!element?.keyed;
|
|
3138
|
+
if (offset.type === 'identifier' && !element?.keyed) {
|
|
2442
3139
|
const indexBinding = indexedBinding(offset.text, object);
|
|
2443
3140
|
const annotation = indexBinding?.type?.namedChild(0);
|
|
2444
3141
|
numeric = annotation?.type === 'predefined_type' && annotation.text === 'number';
|
|
@@ -2447,8 +3144,6 @@ function findCallsInCode(code, parser) {
|
|
|
2447
3144
|
}
|
|
2448
3145
|
}
|
|
2449
3146
|
if (!numeric) return null;
|
|
2450
|
-
const binding = indexedBinding(root.text, object);
|
|
2451
|
-
const element = tsArrayElement(binding?.type);
|
|
2452
3147
|
if (element && !element.qualifier) {
|
|
2453
3148
|
for (let scope = binding.declaration.parent; scope; scope = scope.parent) {
|
|
2454
3149
|
const parameters = scope.childForFieldName('type_parameters');
|
|
@@ -2496,16 +3191,6 @@ function findCallsInCode(code, parser) {
|
|
|
2496
3191
|
const fn = v.childForFieldName('function');
|
|
2497
3192
|
return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
|
|
2498
3193
|
};
|
|
2499
|
-
const _declaresLocalShadow = (declNode, name) => {
|
|
2500
|
-
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
2501
|
-
const d = declNode.namedChild(i);
|
|
2502
|
-
if (d.type !== 'variable_declarator') continue;
|
|
2503
|
-
if (!_patternDeclaresName(d.childForFieldName('name'), name)) continue;
|
|
2504
|
-
if (_isImportBindingInitializer(d.childForFieldName('value'))) continue;
|
|
2505
|
-
return true;
|
|
2506
|
-
}
|
|
2507
|
-
return false;
|
|
2508
|
-
};
|
|
2509
3194
|
|
|
2510
3195
|
// Bare callback references need to distinguish a module-owned VALUE from
|
|
2511
3196
|
// an unbound name. File-level import reachability cannot prove the value's
|
|
@@ -2568,54 +3253,438 @@ function findCallsInCode(code, parser) {
|
|
|
2568
3253
|
// findCallers — let locals and non-symbol arrow params are only
|
|
2569
3254
|
// visible here. Module-level (program) declarations are NOT shadows:
|
|
2570
3255
|
// that's the module binding itself, owned by binding resolution.
|
|
2571
|
-
|
|
2572
|
-
|
|
3256
|
+
// Returns the 1-based line of the shadowing declaration statement, -1
|
|
3257
|
+
// for a parameter / catch / loop binding (never an indexed definition),
|
|
3258
|
+
// 0 when the name is not shadowed. The declaration line lets the query
|
|
3259
|
+
// tell a shadow that IS the pinned definition (declared in any
|
|
3260
|
+
// enclosing function of the reference, fix #397) from an unrelated
|
|
3261
|
+
// local. Statement lists read their memoized binding tables: hoisted
|
|
3262
|
+
// function/class declarations shadow anywhere in the block, lexical and
|
|
3263
|
+
// var declarations from their statement on; a declarator initialized
|
|
3264
|
+
// from require()/import() is the module's binding, not a shadow (#337).
|
|
3265
|
+
// The innermost binding scope containing the reference: a hoisted
|
|
3266
|
+
// function/class declaration anywhere in its block, a lexical or var
|
|
3267
|
+
// declaration from its statement on, a parameter / catch / loop binding
|
|
3268
|
+
// anywhere in its function or statement.
|
|
3269
|
+
const shadowingLineOf = (refNode, name) => {
|
|
3270
|
+
const scopes = bindingScopes.get(name);
|
|
3271
|
+
if (!scopes) return 0;
|
|
3272
|
+
const at = refNode.startIndex;
|
|
3273
|
+
let best = null;
|
|
3274
|
+
for (const scope of scopes) {
|
|
3275
|
+
if (at < scope.start || at >= scope.end) continue;
|
|
3276
|
+
if (scope.declStart != null && scope.declStart >= at) continue;
|
|
3277
|
+
if (!best || scope.end - scope.start < best.end - best.start) best = scope;
|
|
3278
|
+
}
|
|
3279
|
+
return best ? best.line : 0;
|
|
3280
|
+
};
|
|
3281
|
+
const isShadowedByLocal = (refNode, name) => shadowingLineOf(refNode, name) !== 0;
|
|
3282
|
+
// `localShadow` holds the shadowing declaration's line (-1 for a
|
|
3283
|
+
// parameter / catch / loop binding): truthy like the other languages'
|
|
3284
|
+
// `true`, and the query compares the line with the pinned definition.
|
|
3285
|
+
const localShadowFields = (refNode, name) => {
|
|
3286
|
+
const line = shadowingLineOf(refNode, name);
|
|
3287
|
+
return line !== 0 ? { localShadow: line } : null;
|
|
3288
|
+
};
|
|
3289
|
+
|
|
3290
|
+
// The aliased member expression when `refNode` names a live one-hop
|
|
3291
|
+
// const alias: inside the declaring block, after the declaration, and
|
|
3292
|
+
// not shadowed by a parameter or declaration in between (fix #381).
|
|
3293
|
+
const localFieldAliasAt = (refNode) => {
|
|
3294
|
+
const alias = localFieldAliases.get(refNode.text);
|
|
3295
|
+
if (!alias || refNode.startIndex < alias.node.endIndex ||
|
|
3296
|
+
refNode.startIndex < alias.scope.startIndex ||
|
|
3297
|
+
refNode.endIndex > alias.scope.endIndex) return null;
|
|
3298
|
+
const name = refNode.text;
|
|
3299
|
+
for (let p = refNode.parent; p && p.id !== alias.scope.id; p = p.parent) {
|
|
2573
3300
|
if (p.type === 'statement_block') {
|
|
2574
3301
|
for (let i = 0; i < p.namedChildCount; i++) {
|
|
2575
3302
|
const stmt = p.namedChild(i);
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
3303
|
+
if ((stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') &&
|
|
3304
|
+
_declaresName(stmt, name)) return null;
|
|
3305
|
+
if ((stmt.type === 'function_declaration' || stmt.type === 'class_declaration' ||
|
|
3306
|
+
stmt.type === 'generator_function_declaration') &&
|
|
3307
|
+
stmt.childForFieldName('name')?.text === name) return null;
|
|
3308
|
+
}
|
|
3309
|
+
} else if (p.type === 'for_statement' || p.type === 'for_in_statement') {
|
|
3310
|
+
const left = p.childForFieldName('initializer') || p.childForFieldName('left');
|
|
3311
|
+
if (left && (_patternDeclaresName(left, name) ||
|
|
3312
|
+
((left.type === 'lexical_declaration' || left.type === 'variable_declaration') &&
|
|
3313
|
+
_declaresName(left, name)))) return null;
|
|
3314
|
+
} else if (p.type === 'catch_clause') {
|
|
3315
|
+
if (_patternDeclaresName(p.childForFieldName('parameter'), name)) return null;
|
|
3316
|
+
} else if (isFunctionNode(p)) {
|
|
3317
|
+
const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
|
|
3318
|
+
if (params && (_patternDeclaresName(params, name) ||
|
|
3319
|
+
params.namedChildren.some(prm => _patternDeclaresName(prm, name)))) return null;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return alias.node;
|
|
3323
|
+
};
|
|
3324
|
+
|
|
3325
|
+
// Receiver facts of a member access `objNode.propName` (the receiver
|
|
3326
|
+
// half of a method-call record). Shared by method calls and by bare
|
|
3327
|
+
// calls of names destructured from an object (fix #397), which read the
|
|
3328
|
+
// member of the destructuring source.
|
|
3329
|
+
const memberReceiverFacts = (objNode, propName) => {
|
|
3330
|
+
// Extract receiver: handles identifiers (obj), this, super
|
|
3331
|
+
let receiver = undefined;
|
|
3332
|
+
// fix #381: `c.load()` after `const c = this.config`
|
|
3333
|
+
// receives exactly like `this.config.load()` while the
|
|
3334
|
+
// const is the binding in scope.
|
|
3335
|
+
const aliasedReceiver = objNode?.type === 'identifier' &&
|
|
3336
|
+
!localVarTypes.has(objNode.text)
|
|
3337
|
+
? localFieldAliasAt(objNode) : null;
|
|
3338
|
+
if (aliasedReceiver) objNode = aliasedReceiver;
|
|
3339
|
+
if (objNode) {
|
|
3340
|
+
if (objNode.type === 'identifier' || objNode.type === 'this' || objNode.type === 'super') {
|
|
3341
|
+
receiver = objNode.text;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
// One-hop field receiver (fix #219 — #202's shape for
|
|
3345
|
+
// structural): this._map.has(x) / def.cache.get(k) —
|
|
3346
|
+
// receiverRoot/Field let findCallers hop to the
|
|
3347
|
+
// field's DECLARED type annotation. `this`-rooted hops
|
|
3348
|
+
// resolve their root type query-side (the enclosing
|
|
3349
|
+
// class); identifier roots type from local annotations.
|
|
3350
|
+
let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
|
|
3351
|
+
let receiverDeepPath = false;
|
|
3352
|
+
if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
|
|
3353
|
+
if (!receiver && objNode && objNode.type === 'member_expression') {
|
|
3354
|
+
const rootNode = objNode.childForFieldName('object');
|
|
3355
|
+
const fldNode = objNode.childForFieldName('property');
|
|
3356
|
+
if (fldNode && rootNode &&
|
|
3357
|
+
(rootNode.type === 'identifier' || rootNode.type === 'this')) {
|
|
3358
|
+
receiverRoot = rootNode.text;
|
|
3359
|
+
receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
|
|
3360
|
+
receiverFieldName = fldNode.text;
|
|
3361
|
+
if (rootNode.type === 'identifier') {
|
|
3362
|
+
receiverRootType = localVarTypes.get(rootNode.text);
|
|
3363
|
+
}
|
|
3364
|
+
} else {
|
|
3365
|
+
// Preserve unresolved deeper member chains
|
|
3366
|
+
// (`client.req.query()`). Their terminal name
|
|
3367
|
+
// must not borrow a same-file method binding
|
|
3368
|
+
// while the root object's type is unknown.
|
|
3369
|
+
receiverDeepPath = true;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
// Chained receiver (fix #219): the receiver IS a call —
|
|
3373
|
+
// parseAsync(args).catch(...) — record the producer so
|
|
3374
|
+
// findCallers can type the receiver from its declared
|
|
3375
|
+
// return annotation (Promise<...> → Promise).
|
|
3376
|
+
let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
|
|
3377
|
+
let receiverCallStart, receiverCallEnd;
|
|
3378
|
+
{
|
|
3379
|
+
let recvNode = objNode;
|
|
3380
|
+
if (recvNode && recvNode.type === 'parenthesized_expression') {
|
|
3381
|
+
recvNode = recvNode.namedChild(0);
|
|
3382
|
+
}
|
|
3383
|
+
if (recvNode && recvNode.type === 'await_expression') {
|
|
3384
|
+
receiverCallAwaited = true;
|
|
3385
|
+
recvNode = recvNode.namedChild(0);
|
|
3386
|
+
}
|
|
3387
|
+
if (recvNode && recvNode.type === 'call_expression') {
|
|
3388
|
+
const prodFunc = recvNode.childForFieldName('function');
|
|
3389
|
+
if (prodFunc?.type === 'identifier') {
|
|
3390
|
+
receiverCall = prodFunc.text;
|
|
3391
|
+
// Producer link (fix #258): plain-call
|
|
3392
|
+
// records carry the call node's start line
|
|
3393
|
+
receiverCallLine = recvNode.startPosition.row + 1;
|
|
3394
|
+
receiverCallStart = recvNode.startIndex;
|
|
3395
|
+
receiverCallEnd = recvNode.endIndex;
|
|
3396
|
+
} else if (prodFunc?.type === 'member_expression') {
|
|
3397
|
+
const prodProp = prodFunc.childForFieldName('property');
|
|
3398
|
+
if (prodProp) {
|
|
3399
|
+
receiverCall = prodProp.text;
|
|
3400
|
+
receiverCallIsMethod = true;
|
|
3401
|
+
// Method records report the property
|
|
3402
|
+
// node's own line
|
|
3403
|
+
receiverCallLine = prodProp.startPosition.row + 1;
|
|
3404
|
+
receiverCallStart = recvNode.startIndex;
|
|
3405
|
+
receiverCallEnd = recvNode.endIndex;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
if (!receiverCall) receiverCallAwaited = undefined;
|
|
3410
|
+
}
|
|
3411
|
+
// Literal receivers carry their builtin type: [].map() can
|
|
3412
|
+
// never be a project class method
|
|
3413
|
+
// A freshly constructed receiver has an exact runtime
|
|
3414
|
+
// type as well: new Service().start(). Recording it here
|
|
3415
|
+
// avoids treating the call as an untyped method dispatch.
|
|
3416
|
+
const constructedReceiverType = objNode?.type === 'new_expression'
|
|
3417
|
+
? jsConstructorTypeName(objNode.childForFieldName('constructor'))
|
|
3418
|
+
: undefined;
|
|
3419
|
+
const constructedReceiverQualifier = objNode?.type === 'new_expression'
|
|
3420
|
+
? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
|
|
3421
|
+
: undefined;
|
|
3422
|
+
const indexedReceiver = indexedArrayReceiver(objNode);
|
|
3423
|
+
const receiverType = indexedReceiver?.type || (receiver
|
|
3424
|
+
? localVarTypes.get(receiver)
|
|
3425
|
+
: (constructedReceiverType ||
|
|
3426
|
+
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
|
|
3427
|
+
// Module receiver (ns.helper()) — unless locally shadowed
|
|
3428
|
+
// by a typed instance binding
|
|
3429
|
+
const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
|
|
3430
|
+
const receiverIsModule = !!receiverModuleSpecifier ||
|
|
3431
|
+
(!!receiver && moduleAliases.has(receiver) &&
|
|
3432
|
+
!localVarTypes.has(receiver));
|
|
3433
|
+
const receiverModuleComposition = receiver &&
|
|
3434
|
+
!unsafeModuleCompositions.has(receiver)
|
|
3435
|
+
? moduleCompositions.get(receiver) : undefined;
|
|
3436
|
+
return {
|
|
3437
|
+
receiver,
|
|
3438
|
+
...(receiverType && { receiverType,
|
|
3439
|
+
...(indexedReceiver ? { receiverTypeSource: 'annotation',
|
|
3440
|
+
receiverTypeEvidence: indexedReceiver.evidence }
|
|
3441
|
+
: receiver ? localVarTypes.fields(receiver, receiverType) : {
|
|
3442
|
+
receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
|
|
3443
|
+
receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
|
|
3444
|
+
}),
|
|
3445
|
+
}),
|
|
3446
|
+
...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3447
|
+
(receiver && localVarTypeQualifiers.get(receiver))) && {
|
|
3448
|
+
receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3449
|
+
localVarTypeQualifiers.get(receiver),
|
|
3450
|
+
}),
|
|
3451
|
+
...(receiverIsModule && { receiverIsModule: true }),
|
|
3452
|
+
...(receiverModuleSpecifier && { receiverModuleSpecifier }),
|
|
3453
|
+
...(receiverModuleComposition && {
|
|
3454
|
+
receiverModuleComposition,
|
|
3455
|
+
}),
|
|
3456
|
+
...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
|
|
3457
|
+
receiverMemberAssigned: true,
|
|
3458
|
+
}),
|
|
3459
|
+
...(receiverBindingNode &&
|
|
3460
|
+
isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
|
|
3461
|
+
{ receiverLocalBinding: true }),
|
|
3462
|
+
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
3463
|
+
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
3464
|
+
...(receiverDeepPath && { receiverDeepPath: true }),
|
|
3465
|
+
...(receiverCall && { receiverCall }),
|
|
3466
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
3467
|
+
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
3468
|
+
...(receiverCallLine && { receiverCallLine }),
|
|
3469
|
+
...(receiverCallStart != null && { receiverCallStart }),
|
|
3470
|
+
...(receiverCallEnd != null && { receiverCallEnd }),
|
|
3471
|
+
};
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
// Destructured member bindings (fix #397, immer-measured: `const {
|
|
3475
|
+
// produce } = createPatchedImmer()` then `produce(...)` lost every
|
|
3476
|
+
// caller). An object-pattern binding holds the MEMBER of its source
|
|
3477
|
+
// object: `const { run } = make(); run()` reads `make().run`, so the bare
|
|
3478
|
+
// call is that member access. Names some object pattern binds, so the
|
|
3479
|
+
// lexical lookup below runs only for them.
|
|
3480
|
+
// The node declaring `name` in a declaration / parameter pattern.
|
|
3481
|
+
const patternBindingNode = (pattern, name) => {
|
|
3482
|
+
if (!pattern) return null;
|
|
3483
|
+
if (pattern.type === 'required_parameter' || pattern.type === 'optional_parameter') {
|
|
3484
|
+
return patternBindingNode(pattern.childForFieldName('pattern') ||
|
|
3485
|
+
pattern.childForFieldName('name'), name);
|
|
3486
|
+
}
|
|
3487
|
+
if (pattern.type === 'identifier' || pattern.type === 'shorthand_property_identifier_pattern') {
|
|
3488
|
+
return pattern.text === name ? pattern : null;
|
|
3489
|
+
}
|
|
3490
|
+
if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
|
|
3491
|
+
return patternBindingNode(pattern.childForFieldName('value'), name);
|
|
3492
|
+
}
|
|
3493
|
+
if (pattern.type === 'assignment_pattern' || pattern.type === 'object_assignment_pattern') {
|
|
3494
|
+
return patternBindingNode(pattern.childForFieldName('left') ||
|
|
3495
|
+
pattern.childForFieldName('pattern'), name);
|
|
3496
|
+
}
|
|
3497
|
+
if (pattern.type === 'type_annotation' || pattern.type === 'predefined_type') return null;
|
|
3498
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
3499
|
+
const found = patternBindingNode(pattern.namedChild(i), name);
|
|
3500
|
+
if (found) return found;
|
|
3501
|
+
}
|
|
3502
|
+
return null;
|
|
3503
|
+
};
|
|
3504
|
+
const declarationBindingNode = (declaration, name) => {
|
|
3505
|
+
for (let i = 0; i < declaration.namedChildCount; i++) {
|
|
3506
|
+
const declarator = declaration.namedChild(i);
|
|
3507
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
3508
|
+
const found = patternBindingNode(declarator.childForFieldName('name'), name);
|
|
3509
|
+
if (found) return found;
|
|
3510
|
+
}
|
|
3511
|
+
return null;
|
|
3512
|
+
};
|
|
3513
|
+
// The innermost lexical binding of `name` visible at `refNode`: the
|
|
3514
|
+
// declaring identifier / shorthand pattern node, or a marker for a
|
|
3515
|
+
// binding that is not a variable pattern (function, class, import,
|
|
3516
|
+
// named function expression). Within the declaring block a let/const
|
|
3517
|
+
// must precede the reference; from a nested function any position of
|
|
3518
|
+
// the block counts (the closure runs after the block is initialized).
|
|
3519
|
+
const innermostBinding = (refNode, name) => {
|
|
3520
|
+
let crossedFunction = false;
|
|
3521
|
+
for (let p = refNode.parent; p; p = p.parent) {
|
|
3522
|
+
if (p.type === 'statement_block' || p.type === 'program') {
|
|
3523
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
3524
|
+
let stmt = p.namedChild(i);
|
|
3525
|
+
if (stmt.type === 'export_statement') {
|
|
3526
|
+
stmt = stmt.childForFieldName('declaration') || stmt.namedChild(0);
|
|
3527
|
+
if (!stmt) continue;
|
|
3528
|
+
}
|
|
2579
3529
|
if ((stmt.type === 'function_declaration' ||
|
|
2580
3530
|
stmt.type === 'generator_function_declaration' ||
|
|
2581
3531
|
stmt.type === 'class_declaration') &&
|
|
2582
|
-
stmt.childForFieldName('name')?.text === name) return true;
|
|
2583
|
-
if (stmt.
|
|
2584
|
-
|
|
2585
|
-
|
|
3532
|
+
stmt.childForFieldName('name')?.text === name) return { other: true };
|
|
3533
|
+
if (stmt.type === 'import_statement' && p.type === 'program') {
|
|
3534
|
+
const clause = stmt.namedChildren.find(c => c.type === 'import_clause');
|
|
3535
|
+
if (clause && clause.descendantsOfType(['identifier']).some(id => id.text === name)) {
|
|
3536
|
+
return { other: true };
|
|
3537
|
+
}
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
if (stmt.type !== 'lexical_declaration' && stmt.type !== 'variable_declaration') continue;
|
|
3541
|
+
if (!crossedFunction && stmt.startIndex >= refNode.startIndex) continue;
|
|
3542
|
+
const found = declarationBindingNode(stmt, name);
|
|
3543
|
+
if (found) return { node: found };
|
|
2586
3544
|
}
|
|
2587
3545
|
} else if (p.type === 'for_statement') {
|
|
2588
3546
|
const init = p.childForFieldName('initializer');
|
|
2589
|
-
if (init && (init.type === 'lexical_declaration' || init.type === 'variable_declaration')
|
|
2590
|
-
|
|
3547
|
+
if (init && (init.type === 'lexical_declaration' || init.type === 'variable_declaration')) {
|
|
3548
|
+
const found = declarationBindingNode(init, name);
|
|
3549
|
+
if (found) return { node: found };
|
|
3550
|
+
}
|
|
2591
3551
|
} else if (p.type === 'for_in_statement') {
|
|
2592
3552
|
const left = p.childForFieldName('left');
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
3553
|
+
const found = left && (patternBindingNode(left, name) ||
|
|
3554
|
+
((left.type === 'lexical_declaration' || left.type === 'variable_declaration')
|
|
3555
|
+
? declarationBindingNode(left, name) : null));
|
|
3556
|
+
if (found) return { node: found };
|
|
2596
3557
|
} else if (p.type === 'catch_clause') {
|
|
2597
|
-
const
|
|
2598
|
-
if (
|
|
2599
|
-
} else if (p
|
|
2600
|
-
p.type === 'function_declaration' || p.type === 'function' ||
|
|
2601
|
-
p.type === 'method_definition' || p.type === 'generator_function' ||
|
|
2602
|
-
p.type === 'generator_function_declaration') {
|
|
3558
|
+
const found = patternBindingNode(p.childForFieldName('parameter'), name);
|
|
3559
|
+
if (found) return { node: found };
|
|
3560
|
+
} else if (isFunctionNode(p)) {
|
|
2603
3561
|
const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
3562
|
+
const found = params && patternBindingNode(params, name);
|
|
3563
|
+
if (found) return { node: found };
|
|
3564
|
+
if ((p.type === 'function_expression' || p.type === 'function') &&
|
|
3565
|
+
p.childForFieldName('name')?.text === name) return { other: true };
|
|
3566
|
+
crossedFunction = true;
|
|
3567
|
+
} else if (p.type === 'class' && p.childForFieldName('name')?.text === name) {
|
|
3568
|
+
return { other: true };
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
return null;
|
|
3572
|
+
};
|
|
3573
|
+
// The destructuring that binds `name` at `refNode`, when the innermost
|
|
3574
|
+
// binding is an object-pattern property whose source is not an import
|
|
3575
|
+
// (`const { f } = require('./m')` stays an import binding): the property
|
|
3576
|
+
// key, its position, and the receiver facts of the source object read as
|
|
3577
|
+
// `source.key`. A parameter pattern's source is the argument (typed by a
|
|
3578
|
+
// plain TS annotation when present); nested patterns, loop and catch
|
|
3579
|
+
// bindings and defaulted properties have an unknown source.
|
|
3580
|
+
const destructuredBindingOf = (refNode, name) => {
|
|
3581
|
+
if (!destructuredNames.has(name)) return null;
|
|
3582
|
+
const binding = innermostBinding(refNode, name);
|
|
3583
|
+
const node = binding?.node;
|
|
3584
|
+
if (!node) return null;
|
|
3585
|
+
let keyNode = null;
|
|
3586
|
+
let shorthand = false;
|
|
3587
|
+
let hasDefault = false;
|
|
3588
|
+
let property = node;
|
|
3589
|
+
if (node.type === 'shorthand_property_identifier_pattern') {
|
|
3590
|
+
keyNode = node;
|
|
3591
|
+
shorthand = true;
|
|
3592
|
+
if (node.parent?.type === 'object_assignment_pattern') {
|
|
3593
|
+
hasDefault = true;
|
|
3594
|
+
property = node.parent;
|
|
3595
|
+
}
|
|
3596
|
+
} else if (node.type === 'identifier') {
|
|
3597
|
+
let holder = node.parent;
|
|
3598
|
+
if (holder?.type === 'assignment_pattern' &&
|
|
3599
|
+
sameNode(holder.childForFieldName('left'), node)) {
|
|
3600
|
+
hasDefault = true;
|
|
3601
|
+
holder = holder.parent;
|
|
3602
|
+
}
|
|
3603
|
+
if (holder?.type !== 'pair_pattern') return null;
|
|
3604
|
+
const key = holder.childForFieldName('key');
|
|
3605
|
+
if (!key || (key.type !== 'property_identifier' && key.type !== 'identifier')) return null;
|
|
3606
|
+
keyNode = key;
|
|
3607
|
+
property = holder;
|
|
3608
|
+
}
|
|
3609
|
+
const pattern = property.parent;
|
|
3610
|
+
if (!keyNode || pattern?.type !== 'object_pattern') return null;
|
|
3611
|
+
let holder = pattern.parent;
|
|
3612
|
+
let facts = null;
|
|
3613
|
+
let sourceKind = 'unknown';
|
|
3614
|
+
if (holder?.type === 'variable_declarator' &&
|
|
3615
|
+
sameNode(holder.childForFieldName('name'), pattern)) {
|
|
3616
|
+
const value = holder.childForFieldName('value');
|
|
3617
|
+
if (!value || _isImportBindingInitializer(value)) return null;
|
|
3618
|
+
if (!hasDefault) {
|
|
3619
|
+
facts = memberReceiverFacts(value, keyNode.text);
|
|
3620
|
+
sourceKind = 'value';
|
|
3621
|
+
}
|
|
3622
|
+
} else {
|
|
3623
|
+
let typeNode = null;
|
|
3624
|
+
if (holder?.type === 'assignment_pattern' && sameNode(holder.childForFieldName('left'), pattern)) {
|
|
3625
|
+
holder = holder.parent;
|
|
3626
|
+
}
|
|
3627
|
+
if (holder?.type === 'required_parameter' || holder?.type === 'optional_parameter') {
|
|
3628
|
+
typeNode = holder.childForFieldName('type');
|
|
3629
|
+
holder = holder.parent;
|
|
3630
|
+
}
|
|
3631
|
+
if (holder?.type === 'formal_parameters') {
|
|
3632
|
+
sourceKind = 'parameter';
|
|
3633
|
+
const written = typeNode?.namedChild(0);
|
|
3634
|
+
if (!hasDefault && written?.type === 'type_identifier') {
|
|
3635
|
+
facts = { receiverType: written.text, receiverTypeSource: 'annotation',
|
|
3636
|
+
receiverTypeEvidence: typeOrigin('annotation', written) };
|
|
2610
3637
|
}
|
|
2611
3638
|
}
|
|
2612
3639
|
}
|
|
2613
|
-
return
|
|
3640
|
+
return {
|
|
3641
|
+
key: keyNode.text,
|
|
3642
|
+
line: keyNode.startPosition.row + 1,
|
|
3643
|
+
column: keyNode.startPosition.column,
|
|
3644
|
+
...(shorthand && { shorthand: true }),
|
|
3645
|
+
source: sourceKind,
|
|
3646
|
+
receiver: facts || { receiverDeepPath: true },
|
|
3647
|
+
};
|
|
3648
|
+
};
|
|
3649
|
+
// A method taken as a value (fix #397, immer-measured: `export const
|
|
3650
|
+
// produce = immer.produce`): a paren-less member access stored by a
|
|
3651
|
+
// declaration or assignment names the member exactly like a call on the
|
|
3652
|
+
// same receiver. Recorded when the receiver has typing evidence (a typed
|
|
3653
|
+
// local or `this`); an untyped `a.b` read stays a plain reference, since
|
|
3654
|
+
// most such reads are data.
|
|
3655
|
+
const memberValueReference = (valueNode) => {
|
|
3656
|
+
let value = valueNode;
|
|
3657
|
+
while (value && (value.type === 'parenthesized_expression' || value.type === 'as_expression' ||
|
|
3658
|
+
value.type === 'satisfies_expression' || value.type === 'non_null_expression')) {
|
|
3659
|
+
value = value.namedChild(0);
|
|
3660
|
+
}
|
|
3661
|
+
if (value?.type !== 'member_expression') return;
|
|
3662
|
+
const prop = value.childForFieldName('property');
|
|
3663
|
+
const obj = value.childForFieldName('object');
|
|
3664
|
+
if (prop?.type !== 'property_identifier' || !obj ||
|
|
3665
|
+
(obj.type !== 'identifier' && obj.type !== 'this') || SKIP_IDENTS.has(prop.text)) return;
|
|
3666
|
+
const facts = memberReceiverFacts(obj, prop.text);
|
|
3667
|
+
if (!facts.receiverType && facts.receiver !== 'this') return;
|
|
3668
|
+
calls.push({
|
|
3669
|
+
name: prop.text,
|
|
3670
|
+
line: prop.startPosition.row + 1,
|
|
3671
|
+
column: prop.startPosition.column,
|
|
3672
|
+
isMethod: true,
|
|
3673
|
+
...facts,
|
|
3674
|
+
isFunctionReference: true,
|
|
3675
|
+
memberValue: true,
|
|
3676
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
3677
|
+
});
|
|
3678
|
+
};
|
|
3679
|
+
const destructuredFields = (refNode) => {
|
|
3680
|
+
const destructured = destructuredBindingOf(refNode, refNode.text);
|
|
3681
|
+
return destructured ? { destructured } : null;
|
|
2614
3682
|
};
|
|
2615
3683
|
|
|
2616
3684
|
const bareReferenceBindingFields = (refNode) => ({
|
|
2617
|
-
...(
|
|
3685
|
+
...localShadowFields(refNode, refNode.text),
|
|
2618
3686
|
...(moduleValueBindings.has(refNode.text) && { moduleLocalBinding: true }),
|
|
3687
|
+
...destructuredFields(refNode),
|
|
2619
3688
|
});
|
|
2620
3689
|
|
|
2621
3690
|
const isConditionalReassignment = node => {
|
|
@@ -2659,6 +3728,7 @@ function findCallsInCode(code, parser) {
|
|
|
2659
3728
|
if (node.type === 'variable_declarator') {
|
|
2660
3729
|
const nameNode = node.childForFieldName('name');
|
|
2661
3730
|
const initNode = node.childForFieldName('value');
|
|
3731
|
+
memberValueReference(initNode);
|
|
2662
3732
|
// const pkg = require("./lib") — pkg is a module namespace
|
|
2663
3733
|
if (nameNode?.type === 'identifier' && initNode?.type === 'call_expression') {
|
|
2664
3734
|
const fn = initNode.childForFieldName('function');
|
|
@@ -2670,6 +3740,29 @@ function findCallsInCode(code, parser) {
|
|
|
2670
3740
|
// Simple alias: const p = parse
|
|
2671
3741
|
recordAlias(nameNode.text, initNode.text, node);
|
|
2672
3742
|
}
|
|
3743
|
+
// One-hop const alias (fix #381): `const c = cfg` with a typed
|
|
3744
|
+
// cfg carries cfg's type; `const c = this.config` receives like
|
|
3745
|
+
// `this.config`. Only `const` (one binding, never reassigned).
|
|
3746
|
+
if (nameNode?.type === 'identifier') localFieldAliases.delete(nameNode.text);
|
|
3747
|
+
if (nameNode?.type === 'identifier' && !node.childForFieldName('type') &&
|
|
3748
|
+
node.parent?.type === 'lexical_declaration' &&
|
|
3749
|
+
node.parent.child(0)?.type === 'const' && initNode &&
|
|
3750
|
+
initNode.text !== nameNode.text) {
|
|
3751
|
+
if (initNode.type === 'identifier' && localVarTypes.has(initNode.text)) {
|
|
3752
|
+
localVarTypes.set(nameNode.text, localVarTypes.get(initNode.text),
|
|
3753
|
+
localVarTypes.origins.get(initNode.text) || 'flow');
|
|
3754
|
+
if (localVarTypeQualifiers.has(initNode.text)) {
|
|
3755
|
+
localVarTypeQualifiers.set(nameNode.text, localVarTypeQualifiers.get(initNode.text));
|
|
3756
|
+
} else {
|
|
3757
|
+
localVarTypeQualifiers.delete(nameNode.text);
|
|
3758
|
+
}
|
|
3759
|
+
} else if (initNode.type === 'member_expression' &&
|
|
3760
|
+
initNode.childForFieldName('property')?.type === 'property_identifier' &&
|
|
3761
|
+
['this', 'identifier'].includes(initNode.childForFieldName('object')?.type) &&
|
|
3762
|
+
node.parent.parent) {
|
|
3763
|
+
localFieldAliases.set(nameNode.text, { node: initNode, scope: node.parent.parent });
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
2673
3766
|
// Ternary alias: const fn = cond ? parseCSV : parseJSON → both targets
|
|
2674
3767
|
if (nameNode?.type === 'identifier' && initNode?.type === 'ternary_expression') {
|
|
2675
3768
|
const consequence = initNode.childForFieldName('consequence');
|
|
@@ -2729,6 +3822,17 @@ function findCallsInCode(code, parser) {
|
|
|
2729
3822
|
localVarTypeQualifiers.delete(nameNode.text);
|
|
2730
3823
|
}
|
|
2731
3824
|
}
|
|
3825
|
+
} else if (initNode?.type === 'subscript_expression' &&
|
|
3826
|
+
indexedArrayReceiver(initNode)) {
|
|
3827
|
+
// Declared container element (fix #359): `const c =
|
|
3828
|
+
// items[0]` with `items: Conv[]` binds a Conv.
|
|
3829
|
+
const element = indexedArrayReceiver(initNode);
|
|
3830
|
+
localVarTypes.set(nameNode.text, element.type, 'annotation', element.node);
|
|
3831
|
+
if (element.qualifier) {
|
|
3832
|
+
localVarTypeQualifiers.set(nameNode.text, element.qualifier);
|
|
3833
|
+
} else {
|
|
3834
|
+
localVarTypeQualifiers.delete(nameNode.text);
|
|
3835
|
+
}
|
|
2732
3836
|
} else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
|
|
2733
3837
|
// Literal declaration types the variable (fix #262):
|
|
2734
3838
|
// `const lines = []` → Array. Annotation, when present,
|
|
@@ -2762,6 +3866,7 @@ function findCallsInCode(code, parser) {
|
|
|
2762
3866
|
if (node.type === 'assignment_expression') {
|
|
2763
3867
|
const left = node.childForFieldName('left');
|
|
2764
3868
|
const right = node.childForFieldName('right');
|
|
3869
|
+
memberValueReference(right);
|
|
2765
3870
|
if (left?.type === 'identifier') {
|
|
2766
3871
|
if (right?.type === 'new_expression') {
|
|
2767
3872
|
nonCallableNames.add(left.text);
|
|
@@ -2884,6 +3989,7 @@ function findCallsInCode(code, parser) {
|
|
|
2884
3989
|
const optionsMethod = funcNode.text === 'fetch'
|
|
2885
3990
|
? getOptionsMethod(node, 1)
|
|
2886
3991
|
: null;
|
|
3992
|
+
const requestConfig = getRequestConfig(node);
|
|
2887
3993
|
calls.push({
|
|
2888
3994
|
name: funcNode.text,
|
|
2889
3995
|
...(resolvedName && { resolvedName }),
|
|
@@ -2892,12 +3998,19 @@ function findCallsInCode(code, parser) {
|
|
|
2892
3998
|
callStart: node.startIndex,
|
|
2893
3999
|
callEnd: node.endIndex,
|
|
2894
4000
|
isMethod: false,
|
|
4001
|
+
// A local binding of the name (fix #397): the call runs
|
|
4002
|
+
// the local's value, not a same-name outer definition.
|
|
4003
|
+
...localShadowFields(funcNode, funcNode.text),
|
|
4004
|
+
...destructuredFields(funcNode),
|
|
2895
4005
|
...(assignedTo && { assignedTo }),
|
|
2896
4006
|
...assignedIterFields,
|
|
2897
4007
|
enclosingFunction,
|
|
2898
|
-
|
|
4008
|
+
// Recorded only when true (fix #397: a `false` on every
|
|
4009
|
+
// record was a twelfth of a JS calls cache).
|
|
4010
|
+
...(uncertain && { uncertain: true }),
|
|
2899
4011
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
2900
|
-
...(optionsMethod && { optionsMethod })
|
|
4012
|
+
...(optionsMethod && { optionsMethod }),
|
|
4013
|
+
...(requestConfig && { requestConfig })
|
|
2901
4014
|
});
|
|
2902
4015
|
} else if (funcNode.type === 'super') {
|
|
2903
4016
|
// super(config) — the subclass constructor invoking the
|
|
@@ -2914,12 +4027,11 @@ function findCallsInCode(code, parser) {
|
|
|
2914
4027
|
receiver: 'super',
|
|
2915
4028
|
argCount: node.childForFieldName('arguments')?.namedChildCount ?? 0,
|
|
2916
4029
|
enclosingFunction,
|
|
2917
|
-
uncertain: false,
|
|
2918
4030
|
});
|
|
2919
4031
|
} else if (funcNode.type === 'member_expression') {
|
|
2920
4032
|
// Method call: obj.foo() or foo.call/apply/bind()
|
|
2921
4033
|
const propNode = funcNode.childForFieldName('property');
|
|
2922
|
-
|
|
4034
|
+
let objNode = funcNode.childForFieldName('object');
|
|
2923
4035
|
|
|
2924
4036
|
if (propNode) {
|
|
2925
4037
|
const propName = propNode.text;
|
|
@@ -2938,6 +4050,7 @@ function findCallsInCode(code, parser) {
|
|
|
2938
4050
|
line: node.startPosition.row + 1,
|
|
2939
4051
|
isMethod: false,
|
|
2940
4052
|
boundCall: true,
|
|
4053
|
+
...destructuredFields(objNode),
|
|
2941
4054
|
enclosingFunction
|
|
2942
4055
|
});
|
|
2943
4056
|
} else if (objNode.type === 'member_expression') {
|
|
@@ -2976,112 +4089,25 @@ function findCallsInCode(code, parser) {
|
|
|
2976
4089
|
receiverLocalBinding: true,
|
|
2977
4090
|
}),
|
|
2978
4091
|
enclosingFunction,
|
|
2979
|
-
uncertain
|
|
4092
|
+
...(uncertain && { uncertain: true }),
|
|
2980
4093
|
});
|
|
2981
4094
|
}
|
|
2982
4095
|
}
|
|
2983
4096
|
} else {
|
|
2984
4097
|
// Regular method call: obj.foo()
|
|
2985
|
-
|
|
2986
|
-
let receiver = undefined;
|
|
2987
|
-
if (objNode) {
|
|
2988
|
-
if (objNode.type === 'identifier' || objNode.type === 'this' || objNode.type === 'super') {
|
|
2989
|
-
receiver = objNode.text;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
// One-hop field receiver (fix #219 — #202's shape for
|
|
2993
|
-
// structural): this._map.has(x) / def.cache.get(k) —
|
|
2994
|
-
// receiverRoot/Field let findCallers hop to the
|
|
2995
|
-
// field's DECLARED type annotation. `this`-rooted hops
|
|
2996
|
-
// resolve their root type query-side (the enclosing
|
|
2997
|
-
// class); identifier roots type from local annotations.
|
|
2998
|
-
let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
|
|
2999
|
-
let receiverDeepPath = false;
|
|
3000
|
-
if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
|
|
3001
|
-
if (!receiver && objNode && objNode.type === 'member_expression') {
|
|
3002
|
-
const rootNode = objNode.childForFieldName('object');
|
|
3003
|
-
const fldNode = objNode.childForFieldName('property');
|
|
3004
|
-
if (fldNode && rootNode &&
|
|
3005
|
-
(rootNode.type === 'identifier' || rootNode.type === 'this')) {
|
|
3006
|
-
receiverRoot = rootNode.text;
|
|
3007
|
-
receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
|
|
3008
|
-
receiverFieldName = fldNode.text;
|
|
3009
|
-
if (rootNode.type === 'identifier') {
|
|
3010
|
-
receiverRootType = localVarTypes.get(rootNode.text);
|
|
3011
|
-
}
|
|
3012
|
-
} else {
|
|
3013
|
-
// Preserve unresolved deeper member chains
|
|
3014
|
-
// (`client.req.query()`). Their terminal name
|
|
3015
|
-
// must not borrow a same-file method binding
|
|
3016
|
-
// while the root object's type is unknown.
|
|
3017
|
-
receiverDeepPath = true;
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
// Chained receiver (fix #219): the receiver IS a call —
|
|
3021
|
-
// parseAsync(args).catch(...) — record the producer so
|
|
3022
|
-
// findCallers can type the receiver from its declared
|
|
3023
|
-
// return annotation (Promise<...> → Promise).
|
|
3024
|
-
let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
|
|
3025
|
-
let receiverCallStart, receiverCallEnd;
|
|
3026
|
-
{
|
|
3027
|
-
let recvNode = objNode;
|
|
3028
|
-
if (recvNode && recvNode.type === 'parenthesized_expression') {
|
|
3029
|
-
recvNode = recvNode.namedChild(0);
|
|
3030
|
-
}
|
|
3031
|
-
if (recvNode && recvNode.type === 'await_expression') {
|
|
3032
|
-
receiverCallAwaited = true;
|
|
3033
|
-
recvNode = recvNode.namedChild(0);
|
|
3034
|
-
}
|
|
3035
|
-
if (recvNode && recvNode.type === 'call_expression') {
|
|
3036
|
-
const prodFunc = recvNode.childForFieldName('function');
|
|
3037
|
-
if (prodFunc?.type === 'identifier') {
|
|
3038
|
-
receiverCall = prodFunc.text;
|
|
3039
|
-
// Producer link (fix #258): plain-call
|
|
3040
|
-
// records carry the call node's start line
|
|
3041
|
-
receiverCallLine = recvNode.startPosition.row + 1;
|
|
3042
|
-
receiverCallStart = recvNode.startIndex;
|
|
3043
|
-
receiverCallEnd = recvNode.endIndex;
|
|
3044
|
-
} else if (prodFunc?.type === 'member_expression') {
|
|
3045
|
-
const prodProp = prodFunc.childForFieldName('property');
|
|
3046
|
-
if (prodProp) {
|
|
3047
|
-
receiverCall = prodProp.text;
|
|
3048
|
-
receiverCallIsMethod = true;
|
|
3049
|
-
// Method records report the property
|
|
3050
|
-
// node's own line
|
|
3051
|
-
receiverCallLine = prodProp.startPosition.row + 1;
|
|
3052
|
-
receiverCallStart = recvNode.startIndex;
|
|
3053
|
-
receiverCallEnd = recvNode.endIndex;
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
if (!receiverCall) receiverCallAwaited = undefined;
|
|
3058
|
-
}
|
|
3059
|
-
// Literal receivers carry their builtin type: [].map() can
|
|
3060
|
-
// never be a project class method
|
|
3061
|
-
// A freshly constructed receiver has an exact runtime
|
|
3062
|
-
// type as well: new Service().start(). Recording it here
|
|
3063
|
-
// avoids treating the call as an untyped method dispatch.
|
|
3064
|
-
const constructedReceiverType = objNode?.type === 'new_expression'
|
|
3065
|
-
? jsConstructorTypeName(objNode.childForFieldName('constructor'))
|
|
3066
|
-
: undefined;
|
|
3067
|
-
const constructedReceiverQualifier = objNode?.type === 'new_expression'
|
|
3068
|
-
? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
|
|
3069
|
-
: undefined;
|
|
3070
|
-
const indexedReceiver = indexedArrayReceiver(objNode);
|
|
3071
|
-
const receiverType = indexedReceiver?.type || (receiver
|
|
3072
|
-
? localVarTypes.get(receiver)
|
|
3073
|
-
: (constructedReceiverType ||
|
|
3074
|
-
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
|
|
3075
|
-
// Module receiver (ns.helper()) — unless locally shadowed
|
|
3076
|
-
// by a typed instance binding
|
|
3077
|
-
const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
|
|
3078
|
-
const receiverIsModule = !!receiverModuleSpecifier ||
|
|
3079
|
-
(!!receiver && moduleAliases.has(receiver) &&
|
|
3080
|
-
!localVarTypes.has(receiver));
|
|
3081
|
-
const receiverModuleComposition = receiver &&
|
|
3082
|
-
!unsafeModuleCompositions.has(receiver)
|
|
3083
|
-
? moduleCompositions.get(receiver) : undefined;
|
|
4098
|
+
const receiverFacts = memberReceiverFacts(objNode, propName);
|
|
3084
4099
|
const firstArg = getFirstStringArg(node);
|
|
4100
|
+
const requestConfig = getRequestConfig(node);
|
|
4101
|
+
const handlerArgs = firstArg && ROUTE_VERB_METHODS.test(propNode.text) &&
|
|
4102
|
+
getArgCount(node) >= 2 ? getHandlerArgs(node) : null;
|
|
4103
|
+
// fix #366: `server.register(plugin, { prefix })` mounts
|
|
4104
|
+
// the plugin's routes under the prefix.
|
|
4105
|
+
const registerMount = propNode.text === 'register' ? getRegisterMount(node) : null;
|
|
4106
|
+
// fix #366: the argument shapes of a router mount
|
|
4107
|
+
// (`app.use('/api', router)`, `app.route('/v2', api)`):
|
|
4108
|
+
// identifier names, or '()' for any other expression.
|
|
4109
|
+
const mountArgs = (propNode.text === 'use' || propNode.text === 'route') &&
|
|
4110
|
+
getArgCount(node) >= 2 ? getMountArgs(node) : null;
|
|
3085
4111
|
const argCount = getArgCount(node);
|
|
3086
4112
|
let assignedTo = jsAssignmentTargetOf(node);
|
|
3087
4113
|
let assignedIterFields = {};
|
|
@@ -3106,45 +4132,20 @@ function findCallsInCode(code, parser) {
|
|
|
3106
4132
|
callStart: node.startIndex,
|
|
3107
4133
|
callEnd: node.endIndex,
|
|
3108
4134
|
isMethod: true,
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
|
|
3115
|
-
receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
|
|
3116
|
-
}),
|
|
3117
|
-
}),
|
|
3118
|
-
...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3119
|
-
(receiver && localVarTypeQualifiers.get(receiver))) && {
|
|
3120
|
-
receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3121
|
-
localVarTypeQualifiers.get(receiver),
|
|
3122
|
-
}),
|
|
3123
|
-
...(receiverIsModule && { receiverIsModule: true }),
|
|
3124
|
-
...(receiverModuleSpecifier && { receiverModuleSpecifier }),
|
|
3125
|
-
...(receiverModuleComposition && {
|
|
3126
|
-
receiverModuleComposition,
|
|
3127
|
-
}),
|
|
3128
|
-
...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
|
|
3129
|
-
receiverMemberAssigned: true,
|
|
3130
|
-
}),
|
|
3131
|
-
...(receiverBindingNode &&
|
|
3132
|
-
isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
|
|
3133
|
-
{ receiverLocalBinding: true }),
|
|
3134
|
-
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
3135
|
-
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
3136
|
-
...(receiverDeepPath && { receiverDeepPath: true }),
|
|
3137
|
-
...(receiverCall && { receiverCall }),
|
|
3138
|
-
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
3139
|
-
...(receiverCallAwaited && { receiverCallAwaited: true }),
|
|
3140
|
-
...(receiverCallLine && { receiverCallLine }),
|
|
3141
|
-
...(receiverCallStart != null && { receiverCallStart }),
|
|
3142
|
-
...(receiverCallEnd != null && { receiverCallEnd }),
|
|
4135
|
+
// `await x.m()` and `f(x.m())` (fix #402): the
|
|
4136
|
+
// value is awaited or passed on; audit-async skips them.
|
|
4137
|
+
...(directlyAwaited(node, 'await_expression') && { awaited: true }),
|
|
4138
|
+
...(directlyAwaited(node, 'arguments') && { passedAsArgument: true }),
|
|
4139
|
+
...receiverFacts,
|
|
3143
4140
|
...(assignedTo && { assignedTo }),
|
|
3144
4141
|
...assignedIterFields,
|
|
3145
4142
|
enclosingFunction,
|
|
3146
|
-
uncertain,
|
|
4143
|
+
...(uncertain && { uncertain: true }),
|
|
3147
4144
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
4145
|
+
...(requestConfig && { requestConfig }),
|
|
4146
|
+
...(registerMount && { registerMount }),
|
|
4147
|
+
...(mountArgs && { mountArgs }),
|
|
4148
|
+
...(handlerArgs && { handlerArgs }),
|
|
3148
4149
|
argCount
|
|
3149
4150
|
});
|
|
3150
4151
|
}
|
|
@@ -3288,13 +4289,17 @@ function findCallsInCode(code, parser) {
|
|
|
3288
4289
|
if (ctorNode) {
|
|
3289
4290
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
3290
4291
|
|
|
4292
|
+
// fix #366: `new Router({ prefix: '/api' })` (koa-router)
|
|
4293
|
+
// carries a constructor mount prefix.
|
|
4294
|
+
const ctorPrefix = hasObjectKeyArg(node, 'prefix');
|
|
3291
4295
|
if (ctorNode.type === 'identifier') {
|
|
3292
4296
|
calls.push({
|
|
3293
4297
|
name: ctorNode.text,
|
|
3294
4298
|
line: node.startPosition.row + 1,
|
|
3295
4299
|
isMethod: false,
|
|
3296
4300
|
isConstructor: true,
|
|
3297
|
-
...(
|
|
4301
|
+
...localShadowFields(ctorNode, ctorNode.text),
|
|
4302
|
+
...(ctorPrefix && { prefixOption: true }),
|
|
3298
4303
|
enclosingFunction
|
|
3299
4304
|
});
|
|
3300
4305
|
} else if (ctorNode.type === 'member_expression') {
|
|
@@ -3306,6 +4311,7 @@ function findCallsInCode(code, parser) {
|
|
|
3306
4311
|
line: node.startPosition.row + 1,
|
|
3307
4312
|
isMethod: true,
|
|
3308
4313
|
isConstructor: true,
|
|
4314
|
+
...(ctorPrefix && { prefixOption: true }),
|
|
3309
4315
|
enclosingFunction
|
|
3310
4316
|
});
|
|
3311
4317
|
}
|
|
@@ -3730,6 +4736,7 @@ function findImportsInCode(code, parser) {
|
|
|
3730
4736
|
const names = [];
|
|
3731
4737
|
const esmRenames = [];
|
|
3732
4738
|
let importType = 'named';
|
|
4739
|
+
let defaultName = null;
|
|
3733
4740
|
let typeOnly = hasTypeKeyword(node);
|
|
3734
4741
|
let specifierCount = 0;
|
|
3735
4742
|
let typeSpecifierCount = 0;
|
|
@@ -3764,9 +4771,11 @@ function findImportsInCode(code, parser) {
|
|
|
3764
4771
|
for (let j = 0; j < child.namedChildCount; j++) {
|
|
3765
4772
|
const clauseChild = child.namedChild(j);
|
|
3766
4773
|
if (clauseChild.type === 'identifier') {
|
|
3767
|
-
// Default import: import foo from 'x'
|
|
3768
|
-
|
|
3769
|
-
|
|
4774
|
+
// Default import: import foo from 'x'. Beside
|
|
4775
|
+
// named or namespace imports it is its own
|
|
4776
|
+
// record, so its binding stays a default one
|
|
4777
|
+
// (fix #402).
|
|
4778
|
+
defaultName = clauseChild.text;
|
|
3770
4779
|
hasValueBinding = true;
|
|
3771
4780
|
} else if (clauseChild.type === 'named_imports') {
|
|
3772
4781
|
// Named imports: import { a, b } from 'x'
|
|
@@ -3800,6 +4809,13 @@ function findImportsInCode(code, parser) {
|
|
|
3800
4809
|
}
|
|
3801
4810
|
|
|
3802
4811
|
if (modulePath) {
|
|
4812
|
+
if (defaultName && names.length === 0) {
|
|
4813
|
+
names.push(defaultName);
|
|
4814
|
+
importType = 'default';
|
|
4815
|
+
} else if (defaultName) {
|
|
4816
|
+
imports.push({ module: modulePath, names: [defaultName], type: 'default', line,
|
|
4817
|
+
...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
|
|
4818
|
+
}
|
|
3803
4819
|
if (names.length === 0) {
|
|
3804
4820
|
// Side-effect import: import 'x'
|
|
3805
4821
|
importType = 'side-effect';
|
|
@@ -3808,9 +4824,11 @@ function findImportsInCode(code, parser) {
|
|
|
3808
4824
|
importType === 'named' && !hasValueBinding) {
|
|
3809
4825
|
typeOnly = true;
|
|
3810
4826
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
4827
|
+
if (names.length > 0 || !defaultName) {
|
|
4828
|
+
imports.push({ module: modulePath, names, type: importType, line,
|
|
4829
|
+
...(esmRenames.length > 0 && { renames: esmRenames }),
|
|
4830
|
+
...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
|
|
4831
|
+
}
|
|
3814
4832
|
}
|
|
3815
4833
|
return true;
|
|
3816
4834
|
}
|
|
@@ -3885,6 +4903,28 @@ function findImportsInCode(code, parser) {
|
|
|
3885
4903
|
// Check parent for variable name
|
|
3886
4904
|
let parent = node.parent;
|
|
3887
4905
|
let defaultLike = false;
|
|
4906
|
+
// `const stringify = require('url').format` binds the
|
|
4907
|
+
// module's member `format` under the local name (fix
|
|
4908
|
+
// #397, koa-measured): a renamed named import, never the
|
|
4909
|
+
// module value nor a binding of `stringify` itself.
|
|
4910
|
+
const memberOfRequire = parent?.type === 'member_expression' &&
|
|
4911
|
+
sameNode(parent.childForFieldName('object'), node) &&
|
|
4912
|
+
parent.childForFieldName('property')?.type === 'property_identifier' &&
|
|
4913
|
+
parent.parent?.type === 'variable_declarator' &&
|
|
4914
|
+
sameNode(parent.parent.childForFieldName('value'), parent)
|
|
4915
|
+
? parent.childForFieldName('property').text : null;
|
|
4916
|
+
if (memberOfRequire) {
|
|
4917
|
+
const nameNode = parent.parent.childForFieldName('name');
|
|
4918
|
+
if (nameNode?.type === 'identifier') {
|
|
4919
|
+
names.push(memberOfRequire);
|
|
4920
|
+
if (nameNode.text !== memberOfRequire) {
|
|
4921
|
+
if (!importAliases) importAliases = [];
|
|
4922
|
+
importAliases.push({ original: memberOfRequire, local: nameNode.text });
|
|
4923
|
+
renames.push({ original: memberOfRequire, local: nameNode.text });
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
parent = null;
|
|
4927
|
+
}
|
|
3888
4928
|
if (parent && parent.type === 'variable_declarator') {
|
|
3889
4929
|
const nameNode = parent.childForFieldName('name');
|
|
3890
4930
|
if (nameNode) {
|
|
@@ -4281,9 +5321,24 @@ function findExportsInCode(code, parser) {
|
|
|
4281
5321
|
* @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
|
|
4282
5322
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
4283
5323
|
*/
|
|
4284
|
-
|
|
5324
|
+
/** A shorthand property of a module export object (`module.exports = { f }`, `export default { f }`). */
|
|
5325
|
+
function jsExportObjectShorthand(node) {
|
|
5326
|
+
const object = node.parent;
|
|
5327
|
+
if (object?.type !== 'object') return false;
|
|
5328
|
+
const holder = object.parent;
|
|
5329
|
+
if (holder?.type === 'export_statement') return true;
|
|
5330
|
+
if (holder?.type !== 'assignment_expression' ||
|
|
5331
|
+
!sameNode(holder.childForFieldName('right'), object)) return false;
|
|
5332
|
+
const left = holder.childForFieldName('left')?.text?.replace(/\s+/g, '') || '';
|
|
5333
|
+
return left === 'module.exports' || left === 'exports' ||
|
|
5334
|
+
left.startsWith('module.exports.') || left.startsWith('exports.');
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
function findUsagesInCode(code, name, parser, tree, options = {}) {
|
|
4285
5338
|
tree = tree || parseTree(parser, code);
|
|
4286
5339
|
const usages = [];
|
|
5340
|
+
// Lexical scope verdicts (fix #392) only for refactoring internals.
|
|
5341
|
+
const scopeMemo = options.lexicalScopes ? new Map() : null;
|
|
4287
5342
|
|
|
4288
5343
|
visitNameNodes(tree, code, name, (node) => {
|
|
4289
5344
|
// Look for identifier, property_identifier (method names in obj.method() calls),
|
|
@@ -4307,6 +5362,7 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
4307
5362
|
|
|
4308
5363
|
// Classify based on parent node
|
|
4309
5364
|
let usageType = 'reference';
|
|
5365
|
+
let importAliasLocal = false;
|
|
4310
5366
|
|
|
4311
5367
|
if (parent) {
|
|
4312
5368
|
// Import: identifier inside import_specifier or import_clause
|
|
@@ -4314,6 +5370,11 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
4314
5370
|
parent.type === 'import_clause' ||
|
|
4315
5371
|
parent.type === 'namespace_import') {
|
|
4316
5372
|
usageType = 'import';
|
|
5373
|
+
// `import { g as f }`: f is the importer's local alias.
|
|
5374
|
+
const imported = parent.type === 'import_specifier' ? parent.childForFieldName('name') : null;
|
|
5375
|
+
if (imported && sameNode(parent.childForFieldName('alias'), node) && imported.text !== node.text) {
|
|
5376
|
+
importAliasLocal = true;
|
|
5377
|
+
}
|
|
4317
5378
|
}
|
|
4318
5379
|
// Call: identifier is function in call_expression
|
|
4319
5380
|
else if (parent.type === 'call_expression' &&
|
|
@@ -4344,6 +5405,8 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
4344
5405
|
}
|
|
4345
5406
|
// Unwrap require('./x').member — still an import binding
|
|
4346
5407
|
if (value && value.type === 'member_expression') {
|
|
5408
|
+
// `const f = require('m').g`: f is a local alias of g.
|
|
5409
|
+
if (value.childForFieldName('property')?.text !== node.text) importAliasLocal = true;
|
|
4347
5410
|
value = value.childForFieldName('object');
|
|
4348
5411
|
}
|
|
4349
5412
|
if (value && value.type === 'call_expression') {
|
|
@@ -4422,7 +5485,17 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
4422
5485
|
}
|
|
4423
5486
|
}
|
|
4424
5487
|
|
|
4425
|
-
|
|
5488
|
+
// Where a bare reference resolves (fix #392).
|
|
5489
|
+
const scope = scopeMemo && usageType === 'reference'
|
|
5490
|
+
? scopeFields(referenceScope(node, 'javascript', scopeMemo)) : null;
|
|
5491
|
+
// An object-literal shorthand `{ name }` is a key AND a reference to
|
|
5492
|
+
// the binding (fix #397): a rename of the binding keeps the key
|
|
5493
|
+
// (`{ name: renamed }`), except in a module's export object, whose
|
|
5494
|
+
// keys are the export names a rename follows into every importer.
|
|
5495
|
+
const shorthandProperty = scopeMemo && node.type === 'shorthand_property_identifier' &&
|
|
5496
|
+
!jsExportObjectShorthand(node);
|
|
5497
|
+
usages.push({ line, column, usageType, ...scope, ...(shorthandProperty && { shorthandProperty: true }),
|
|
5498
|
+
...(importAliasLocal && usageType === 'import' && { importAlias: true }) });
|
|
4426
5499
|
return true;
|
|
4427
5500
|
});
|
|
4428
5501
|
|