ucn 3.8.23 → 3.8.26
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 +127 -12
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1095 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -52
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/languages/javascript.js
CHANGED
|
@@ -11,7 +11,8 @@ const {
|
|
|
11
11
|
nodeToLocation,
|
|
12
12
|
extractParams,
|
|
13
13
|
parseStructuredParams,
|
|
14
|
-
extractJSDocstring
|
|
14
|
+
extractJSDocstring,
|
|
15
|
+
buildTypeAnnotations
|
|
15
16
|
} = require('./utils');
|
|
16
17
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
17
18
|
|
|
@@ -95,19 +96,138 @@ function extractModifiers(text) {
|
|
|
95
96
|
*/
|
|
96
97
|
function extractDecorators(node) {
|
|
97
98
|
const decorators = [];
|
|
99
|
+
const consume = (n) => {
|
|
100
|
+
if (n.type !== 'decorator') return;
|
|
101
|
+
let text = n.text.replace(/^@/, '');
|
|
102
|
+
const parenIdx = text.indexOf('(');
|
|
103
|
+
if (parenIdx > 0) text = text.substring(0, parenIdx);
|
|
104
|
+
decorators.push(text);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// 1. Direct children — covers most class/method decorators.
|
|
98
108
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
consume(node.namedChild(i));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. When a class/function is wrapped in `export class …`, tree-sitter
|
|
113
|
+
// wraps it in an `export_statement`. The decorator becomes a sibling
|
|
114
|
+
// of the inner declaration *inside* that export_statement. Walk the
|
|
115
|
+
// wrapper's children for any decorator preceding the inner node.
|
|
116
|
+
if (node.parent && node.parent.type === 'export_statement') {
|
|
117
|
+
const wrapper = node.parent;
|
|
118
|
+
let myIdx = -1;
|
|
119
|
+
for (let i = 0; i < wrapper.namedChildCount; i++) {
|
|
120
|
+
if (wrapper.namedChild(i).id === node.id) { myIdx = i; break; }
|
|
121
|
+
}
|
|
122
|
+
for (let i = myIdx - 1; i >= 0; i--) {
|
|
123
|
+
const sib = wrapper.namedChild(i);
|
|
124
|
+
if (sib.type === 'decorator') consume(sib);
|
|
125
|
+
else break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. Some grammars place decorators as preceding siblings of the
|
|
130
|
+
// declaration itself (rather than wrapping in export_statement).
|
|
131
|
+
// Walk back from this node within its parent.
|
|
132
|
+
if (node.parent && node.parent.type !== 'export_statement') {
|
|
133
|
+
const parent = node.parent;
|
|
134
|
+
let myIdx = -1;
|
|
135
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
136
|
+
if (parent.namedChild(i).id === node.id) { myIdx = i; break; }
|
|
137
|
+
}
|
|
138
|
+
for (let i = myIdx - 1; i >= 0; i--) {
|
|
139
|
+
const sib = parent.namedChild(i);
|
|
140
|
+
if (sib.type === 'decorator') consume(sib);
|
|
141
|
+
else break;
|
|
106
142
|
}
|
|
107
143
|
}
|
|
108
144
|
return decorators;
|
|
109
145
|
}
|
|
110
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Extract decorators along with their string-literal first argument.
|
|
149
|
+
* Returns array of { name, args, firstStringArg } where:
|
|
150
|
+
* - name is the decorator name (no @)
|
|
151
|
+
* - args is the raw argument text (without outer parens), or null
|
|
152
|
+
* - firstStringArg is the literal value of the first string-literal argument, or null
|
|
153
|
+
*
|
|
154
|
+
* @Get(':id') → { name: 'Get', args: "':id'", firstStringArg: ':id' }
|
|
155
|
+
* @Controller('/api/users') → { name: 'Controller', args: "'/api/users'", firstStringArg: '/api/users' }
|
|
156
|
+
* @Injectable → { name: 'Injectable', args: null, firstStringArg: null }
|
|
157
|
+
*
|
|
158
|
+
* Used by route extraction (NestJS, etc.) — only the firstStringArg is currently
|
|
159
|
+
* consumed by core/bridge.js, but `args` is preserved for future structural-search use.
|
|
160
|
+
*/
|
|
161
|
+
function extractDecoratorsWithArgs(node) {
|
|
162
|
+
const result = [];
|
|
163
|
+
const { extractStringArg } = require('./utils');
|
|
164
|
+
|
|
165
|
+
const consume = (n) => {
|
|
166
|
+
if (n.type !== 'decorator') return;
|
|
167
|
+
// tree-sitter-javascript: decorator has a single 'expression' child.
|
|
168
|
+
// Look for call_expression vs identifier vs member_expression.
|
|
169
|
+
let inner = null;
|
|
170
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
171
|
+
const c = n.namedChild(i);
|
|
172
|
+
if (c.type !== 'comment') { inner = c; break; }
|
|
173
|
+
}
|
|
174
|
+
if (!inner) return;
|
|
175
|
+
|
|
176
|
+
if (inner.type === 'call_expression') {
|
|
177
|
+
const fn = inner.childForFieldName('function');
|
|
178
|
+
const argsNode = inner.childForFieldName('arguments');
|
|
179
|
+
if (!fn || !argsNode) return;
|
|
180
|
+
const name = fn.text;
|
|
181
|
+
// Get raw arg text without the surrounding parens
|
|
182
|
+
const argsText = argsNode.text.replace(/^\(|\)$/g, '');
|
|
183
|
+
// Find first string-literal arg
|
|
184
|
+
let firstStringArg = null;
|
|
185
|
+
for (let j = 0; j < argsNode.namedChildCount; j++) {
|
|
186
|
+
const arg = argsNode.namedChild(j);
|
|
187
|
+
if (arg.type === 'comment') continue;
|
|
188
|
+
const s = extractStringArg(arg);
|
|
189
|
+
if (s && !s.interp) { firstStringArg = s.value; break; }
|
|
190
|
+
if (s) { firstStringArg = s.value; break; }
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
result.push({ name, args: argsText, firstStringArg });
|
|
194
|
+
} else if (inner.type === 'identifier' || inner.type === 'member_expression') {
|
|
195
|
+
// Plain decorator: @Injectable
|
|
196
|
+
result.push({ name: inner.text, args: null, firstStringArg: null });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Same traversal as extractDecorators
|
|
201
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
202
|
+
consume(node.namedChild(i));
|
|
203
|
+
}
|
|
204
|
+
if (node.parent && node.parent.type === 'export_statement') {
|
|
205
|
+
const wrapper = node.parent;
|
|
206
|
+
let myIdx = -1;
|
|
207
|
+
for (let i = 0; i < wrapper.namedChildCount; i++) {
|
|
208
|
+
if (wrapper.namedChild(i).id === node.id) { myIdx = i; break; }
|
|
209
|
+
}
|
|
210
|
+
for (let i = myIdx - 1; i >= 0; i--) {
|
|
211
|
+
const sib = wrapper.namedChild(i);
|
|
212
|
+
if (sib.type === 'decorator') consume(sib);
|
|
213
|
+
else break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (node.parent && node.parent.type !== 'export_statement') {
|
|
217
|
+
const parent = node.parent;
|
|
218
|
+
let myIdx = -1;
|
|
219
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
220
|
+
if (parent.namedChild(i).id === node.id) { myIdx = i; break; }
|
|
221
|
+
}
|
|
222
|
+
for (let i = myIdx - 1; i >= 0; i--) {
|
|
223
|
+
const sib = parent.namedChild(i);
|
|
224
|
+
if (sib.type === 'decorator') consume(sib);
|
|
225
|
+
else break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
111
231
|
// --- Single-pass helpers: extracted from find* callbacks ---
|
|
112
232
|
|
|
113
233
|
/**
|
|
@@ -131,22 +251,28 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
131
251
|
const generics = extractGenerics(node);
|
|
132
252
|
const docstring = extractJSDocstring(lines, startLine);
|
|
133
253
|
const isGen = isGenerator(node);
|
|
254
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
255
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
134
256
|
// Check parent for export status (function_declaration inside export_statement)
|
|
135
257
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
136
258
|
? extractModifiers(node.parent.text)
|
|
137
259
|
: extractModifiers(node.text);
|
|
260
|
+
// Feature B: explicit isAsync flag (auditAsync needs to know whether
|
|
261
|
+
// the fn was declared `async function`).
|
|
262
|
+
const isAsync = modifiers.includes('async');
|
|
138
263
|
|
|
139
264
|
functions.push({
|
|
140
265
|
name: nameNode.text,
|
|
141
266
|
params: extractParams(paramsNode),
|
|
142
|
-
paramsStructured
|
|
267
|
+
paramsStructured,
|
|
143
268
|
startLine,
|
|
144
269
|
endLine,
|
|
145
270
|
indent,
|
|
146
271
|
isArrow: false,
|
|
147
272
|
isGenerator: isGen,
|
|
273
|
+
isAsync,
|
|
148
274
|
modifiers,
|
|
149
|
-
...
|
|
275
|
+
...typeAnno,
|
|
150
276
|
...(generics && { generics }),
|
|
151
277
|
...(docstring && { docstring })
|
|
152
278
|
});
|
|
@@ -167,11 +293,13 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
167
293
|
const returnType = extractReturnType(node);
|
|
168
294
|
const generics = extractGenerics(node);
|
|
169
295
|
const docstring = extractJSDocstring(lines, startLine);
|
|
296
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'typescript');
|
|
297
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
170
298
|
|
|
171
299
|
functions.push({
|
|
172
300
|
name: nameNode.text,
|
|
173
301
|
params: extractParams(paramsNode),
|
|
174
|
-
paramsStructured
|
|
302
|
+
paramsStructured,
|
|
175
303
|
startLine,
|
|
176
304
|
endLine,
|
|
177
305
|
indent,
|
|
@@ -179,7 +307,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
179
307
|
isGenerator: false,
|
|
180
308
|
isSignature: true,
|
|
181
309
|
modifiers: [],
|
|
182
|
-
...
|
|
310
|
+
...typeAnno,
|
|
183
311
|
...(generics && { generics }),
|
|
184
312
|
...(docstring && { docstring })
|
|
185
313
|
});
|
|
@@ -210,22 +338,31 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
210
338
|
const generics = extractGenerics(valueNode);
|
|
211
339
|
const docstring = extractJSDocstring(lines, startLine);
|
|
212
340
|
const isGen = isGenerator(valueNode);
|
|
341
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
342
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
213
343
|
// Check parent for export status (lexical_declaration inside export_statement)
|
|
214
344
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
215
345
|
? extractModifiers(node.parent.text)
|
|
216
346
|
: extractModifiers(node.text);
|
|
347
|
+
// Feature B: detect async — for arrow/fn-expressions the `async`
|
|
348
|
+
// keyword precedes the parameter list on the value node, NOT on
|
|
349
|
+
// the lexical_declaration text. extractModifiers walked the full
|
|
350
|
+
// declaration text, so we double-check the value node directly.
|
|
351
|
+
const valueIsAsync = valueNode.text.trimStart().startsWith('async ');
|
|
352
|
+
const isAsync = valueIsAsync || modifiers.includes('async');
|
|
217
353
|
|
|
218
354
|
functions.push({
|
|
219
355
|
name: nameNode.text,
|
|
220
356
|
params: extractParams(paramsNode),
|
|
221
|
-
paramsStructured
|
|
357
|
+
paramsStructured,
|
|
222
358
|
startLine,
|
|
223
359
|
endLine,
|
|
224
360
|
indent,
|
|
225
361
|
isArrow,
|
|
226
362
|
isGenerator: isGen,
|
|
363
|
+
isAsync,
|
|
227
364
|
modifiers,
|
|
228
|
-
...
|
|
365
|
+
...typeAnno,
|
|
229
366
|
...(generics && { generics }),
|
|
230
367
|
...(docstring && { docstring })
|
|
231
368
|
});
|
|
@@ -255,6 +392,8 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
255
392
|
const returnType = extractReturnType(innerFn);
|
|
256
393
|
const generics = extractGenerics(innerFn);
|
|
257
394
|
const docstring = extractJSDocstring(lines, startLine);
|
|
395
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
396
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
258
397
|
const modifiers = node.parent && node.parent.type === 'export_statement'
|
|
259
398
|
? extractModifiers(node.parent.text)
|
|
260
399
|
: extractModifiers(node.text);
|
|
@@ -262,14 +401,14 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
262
401
|
functions.push({
|
|
263
402
|
name: nameNode.text,
|
|
264
403
|
params: extractParams(paramsNode),
|
|
265
|
-
paramsStructured
|
|
404
|
+
paramsStructured,
|
|
266
405
|
startLine,
|
|
267
406
|
endLine,
|
|
268
407
|
indent,
|
|
269
408
|
isArrow: innerFn.type === 'arrow_function',
|
|
270
409
|
isGenerator: false,
|
|
271
410
|
modifiers,
|
|
272
|
-
...
|
|
411
|
+
...typeAnno,
|
|
273
412
|
...(generics && { generics }),
|
|
274
413
|
...(docstring && { docstring })
|
|
275
414
|
});
|
|
@@ -330,18 +469,20 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
330
469
|
const generics = extractGenerics(rightNode);
|
|
331
470
|
const docstring = extractJSDocstring(lines, startLine);
|
|
332
471
|
const isGen = isGenerator(rightNode);
|
|
472
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
473
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
333
474
|
|
|
334
475
|
functions.push({
|
|
335
476
|
name,
|
|
336
477
|
params: extractParams(paramsNode),
|
|
337
|
-
paramsStructured
|
|
478
|
+
paramsStructured,
|
|
338
479
|
startLine,
|
|
339
480
|
endLine,
|
|
340
481
|
indent,
|
|
341
482
|
isArrow,
|
|
342
483
|
isGenerator: isGen,
|
|
343
484
|
modifiers: [],
|
|
344
|
-
...
|
|
485
|
+
...typeAnno,
|
|
345
486
|
...(generics && { generics }),
|
|
346
487
|
...(docstring && { docstring })
|
|
347
488
|
});
|
|
@@ -368,18 +509,20 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
368
509
|
const generics = extractGenerics(child);
|
|
369
510
|
const docstring = extractJSDocstring(lines, startLine);
|
|
370
511
|
const isGen = isGenerator(child);
|
|
512
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
513
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
|
|
371
514
|
|
|
372
515
|
functions.push({
|
|
373
516
|
name: 'default',
|
|
374
517
|
params: extractParams(paramsNode),
|
|
375
|
-
paramsStructured
|
|
518
|
+
paramsStructured,
|
|
376
519
|
startLine,
|
|
377
520
|
endLine,
|
|
378
521
|
indent,
|
|
379
522
|
isArrow: child.type === 'arrow_function',
|
|
380
523
|
isGenerator: isGen,
|
|
381
524
|
modifiers: ['export', 'default'],
|
|
382
|
-
...
|
|
525
|
+
...typeAnno,
|
|
383
526
|
...(generics && { generics }),
|
|
384
527
|
...(docstring && { docstring })
|
|
385
528
|
});
|
|
@@ -428,6 +571,7 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
428
571
|
const extendsInfo = extractExtends(node);
|
|
429
572
|
const implementsInfo = extractImplements(node);
|
|
430
573
|
const decorators = extractDecorators(node);
|
|
574
|
+
const decoratorsWithArgs = extractDecoratorsWithArgs(node);
|
|
431
575
|
|
|
432
576
|
const isAbstract = node.type === 'abstract_class_declaration';
|
|
433
577
|
classes.push({
|
|
@@ -441,7 +585,8 @@ function _processClass(node, classes, processedRanges, lines) {
|
|
|
441
585
|
...(generics && { generics }),
|
|
442
586
|
...(extendsInfo && { extends: extendsInfo }),
|
|
443
587
|
...(implementsInfo.length > 0 && { implements: implementsInfo }),
|
|
444
|
-
...(decorators.length > 0 && { decorators })
|
|
588
|
+
...(decorators.length > 0 && { decorators }),
|
|
589
|
+
...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs })
|
|
445
590
|
});
|
|
446
591
|
}
|
|
447
592
|
return true;
|
|
@@ -656,15 +801,17 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
656
801
|
if (nameNode) {
|
|
657
802
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
658
803
|
const returnType = extractReturnType(child);
|
|
804
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
805
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
659
806
|
members.push({
|
|
660
807
|
name: nameNode.text,
|
|
661
808
|
params: extractParams(paramsNode),
|
|
662
|
-
paramsStructured
|
|
809
|
+
paramsStructured,
|
|
663
810
|
startLine,
|
|
664
811
|
endLine,
|
|
665
812
|
memberType: 'method',
|
|
666
813
|
isMethod: true,
|
|
667
|
-
...
|
|
814
|
+
...typeAnno
|
|
668
815
|
});
|
|
669
816
|
}
|
|
670
817
|
} else if (child.type === 'property_signature') {
|
|
@@ -773,20 +920,24 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
773
920
|
const isAsync = text.match(/^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:override\s+)?async\s/) !== null;
|
|
774
921
|
const returnType = extractReturnType(child);
|
|
775
922
|
const docstring = extractJSDocstring(code, startLine);
|
|
923
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
924
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
776
925
|
|
|
926
|
+
const decoratorsWithArgs = extractDecoratorsWithArgs(child);
|
|
777
927
|
members.push({
|
|
778
928
|
name,
|
|
779
929
|
params: extractParams(paramsNode),
|
|
780
|
-
paramsStructured
|
|
930
|
+
paramsStructured,
|
|
781
931
|
startLine,
|
|
782
932
|
endLine,
|
|
783
933
|
memberType,
|
|
784
934
|
isAsync,
|
|
785
935
|
isGenerator: isGen,
|
|
786
936
|
isMethod: true, // Mark as method for context() lookups
|
|
787
|
-
...
|
|
937
|
+
...typeAnno,
|
|
788
938
|
...(docstring && { docstring }),
|
|
789
|
-
...(decorators.length > 0 && { decorators })
|
|
939
|
+
...(decorators.length > 0 && { decorators }),
|
|
940
|
+
...(decoratorsWithArgs.length > 0 && { decoratorsWithArgs })
|
|
790
941
|
});
|
|
791
942
|
}
|
|
792
943
|
}
|
|
@@ -799,6 +950,8 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
799
950
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
800
951
|
const returnType = extractReturnType(child);
|
|
801
952
|
const docstring = extractJSDocstring(code, startLine);
|
|
953
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
954
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
802
955
|
// Collect decorators from preceding siblings
|
|
803
956
|
const decorators = [];
|
|
804
957
|
for (let j = i - 1; j >= 0; j--) {
|
|
@@ -813,12 +966,12 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
813
966
|
members.push({
|
|
814
967
|
name: nameNode.text,
|
|
815
968
|
params: extractParams(paramsNode),
|
|
816
|
-
paramsStructured
|
|
969
|
+
paramsStructured,
|
|
817
970
|
startLine,
|
|
818
971
|
endLine,
|
|
819
972
|
memberType: 'abstract',
|
|
820
973
|
isMethod: true,
|
|
821
|
-
...
|
|
974
|
+
...typeAnno,
|
|
822
975
|
...(docstring && { docstring }),
|
|
823
976
|
...(decorators.length > 0 && { decorators })
|
|
824
977
|
});
|
|
@@ -862,16 +1015,18 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
862
1015
|
if (isArrow) {
|
|
863
1016
|
const paramsNode = valueNode.childForFieldName('parameters');
|
|
864
1017
|
const returnType = extractReturnType(valueNode);
|
|
1018
|
+
const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
|
|
1019
|
+
const typeAnno = buildTypeAnnotations(paramsStructured, returnType, code, startLine, true);
|
|
865
1020
|
members.push({
|
|
866
1021
|
name,
|
|
867
1022
|
params: extractParams(paramsNode),
|
|
868
|
-
paramsStructured
|
|
1023
|
+
paramsStructured,
|
|
869
1024
|
startLine,
|
|
870
1025
|
endLine,
|
|
871
1026
|
memberType: name.startsWith('#') ? 'private' : 'field',
|
|
872
1027
|
isArrow: true,
|
|
873
1028
|
isMethod: true, // Arrow fields are callable like methods
|
|
874
|
-
...
|
|
1029
|
+
...typeAnno,
|
|
875
1030
|
...(fieldDecorators.length > 0 && { decorators: fieldDecorators })
|
|
876
1031
|
});
|
|
877
1032
|
} else {
|
|
@@ -1003,6 +1158,76 @@ function findCallsInCode(code, parser) {
|
|
|
1003
1158
|
const localVarTypes = new Map(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
1004
1159
|
const localVarTypesStack = []; // Stack for function-scoped save/restore of localVarTypes
|
|
1005
1160
|
|
|
1161
|
+
// Helper: extract first string-arg literal from a call_expression node.
|
|
1162
|
+
// Used by route extraction to capture path arg of fetch('/path'), app.get('/path', handler) etc.
|
|
1163
|
+
const { extractStringArg: _extractStringArg } = require('./utils');
|
|
1164
|
+
const getFirstStringArg = (callNode) => {
|
|
1165
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1166
|
+
if (!argsNode) return null;
|
|
1167
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1168
|
+
const arg = argsNode.namedChild(i);
|
|
1169
|
+
if (arg.type === 'comment') continue;
|
|
1170
|
+
return _extractStringArg(arg);
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Helper: count the number of (non-comment) arguments in a call_expression.
|
|
1176
|
+
// Used to disambiguate dual-purpose Express APIs (BUG M5):
|
|
1177
|
+
// app.get('/users', handler) → 2 args → route registration
|
|
1178
|
+
// app.get('env') → 1 arg → config getter, NOT a route
|
|
1179
|
+
const getArgCount = (callNode) => {
|
|
1180
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1181
|
+
if (!argsNode) return 0;
|
|
1182
|
+
let count = 0;
|
|
1183
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1184
|
+
const arg = argsNode.namedChild(i);
|
|
1185
|
+
if (arg.type === 'comment') continue;
|
|
1186
|
+
count++;
|
|
1187
|
+
}
|
|
1188
|
+
return count;
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// MEDIUM-5: extract HTTP method from `fetch(url, { method: 'POST' })`
|
|
1192
|
+
// and similar XHR/Request-init shapes. Returns the upper-cased method
|
|
1193
|
+
// string or null. Looks at argument index `argIdx` (default 1, the
|
|
1194
|
+
// options object after the URL).
|
|
1195
|
+
const getOptionsMethod = (callNode, argIdx = 1) => {
|
|
1196
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1197
|
+
if (!argsNode) return null;
|
|
1198
|
+
// Walk named children and pick the argIdx-th non-comment node.
|
|
1199
|
+
let idx = 0;
|
|
1200
|
+
let target = null;
|
|
1201
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1202
|
+
const arg = argsNode.namedChild(i);
|
|
1203
|
+
if (arg.type === 'comment') continue;
|
|
1204
|
+
if (idx === argIdx) { target = arg; break; }
|
|
1205
|
+
idx++;
|
|
1206
|
+
}
|
|
1207
|
+
if (!target || target.type !== 'object') return null;
|
|
1208
|
+
for (let i = 0; i < target.namedChildCount; i++) {
|
|
1209
|
+
const prop = target.namedChild(i);
|
|
1210
|
+
if (prop.type !== 'pair') continue;
|
|
1211
|
+
const keyNode = prop.childForFieldName('key');
|
|
1212
|
+
const valNode = prop.childForFieldName('value');
|
|
1213
|
+
if (!keyNode || !valNode) continue;
|
|
1214
|
+
// Key may be `method`, `'method'`, or `"method"`.
|
|
1215
|
+
let keyName = keyNode.text;
|
|
1216
|
+
if (keyNode.type === 'string' || keyNode.type === 'property_identifier') {
|
|
1217
|
+
keyName = keyName.replace(/^['"`]|['"`]$/g, '');
|
|
1218
|
+
}
|
|
1219
|
+
if (keyName !== 'method') continue;
|
|
1220
|
+
// Value must be a literal string. Skip variables / expressions
|
|
1221
|
+
// (we can't statically resolve those).
|
|
1222
|
+
const v = _extractStringArg(valNode);
|
|
1223
|
+
if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
|
|
1224
|
+
return v.value.toUpperCase();
|
|
1225
|
+
}
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1006
1231
|
// Helper to check if a node is a non-callable literal
|
|
1007
1232
|
const isNonCallableInit = (node) => {
|
|
1008
1233
|
// Primitive literals
|
|
@@ -1219,6 +1444,11 @@ function findCallsInCode(code, parser) {
|
|
|
1219
1444
|
const alias = aliases.get(funcNode.text);
|
|
1220
1445
|
const resolvedName = typeof alias === 'string' ? alias : undefined;
|
|
1221
1446
|
const resolvedNames = Array.isArray(alias) ? alias : undefined;
|
|
1447
|
+
const firstArg = getFirstStringArg(node);
|
|
1448
|
+
// MEDIUM-5: capture explicit method for fetch(url, { method }).
|
|
1449
|
+
const optionsMethod = funcNode.text === 'fetch'
|
|
1450
|
+
? getOptionsMethod(node, 1)
|
|
1451
|
+
: null;
|
|
1222
1452
|
calls.push({
|
|
1223
1453
|
name: funcNode.text,
|
|
1224
1454
|
...(resolvedName && { resolvedName }),
|
|
@@ -1226,7 +1456,9 @@ function findCallsInCode(code, parser) {
|
|
|
1226
1456
|
line: node.startPosition.row + 1,
|
|
1227
1457
|
isMethod: false,
|
|
1228
1458
|
enclosingFunction,
|
|
1229
|
-
uncertain
|
|
1459
|
+
uncertain,
|
|
1460
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
1461
|
+
...(optionsMethod && { optionsMethod })
|
|
1230
1462
|
});
|
|
1231
1463
|
} else if (funcNode.type === 'member_expression') {
|
|
1232
1464
|
// Method call: obj.foo() or foo.call/apply/bind()
|
|
@@ -1271,6 +1503,8 @@ function findCallsInCode(code, parser) {
|
|
|
1271
1503
|
}
|
|
1272
1504
|
}
|
|
1273
1505
|
const receiverType = receiver ? localVarTypes.get(receiver) : undefined;
|
|
1506
|
+
const firstArg = getFirstStringArg(node);
|
|
1507
|
+
const argCount = getArgCount(node);
|
|
1274
1508
|
calls.push({
|
|
1275
1509
|
name: propName,
|
|
1276
1510
|
line: node.startPosition.row + 1,
|
|
@@ -1278,7 +1512,9 @@ function findCallsInCode(code, parser) {
|
|
|
1278
1512
|
receiver,
|
|
1279
1513
|
...(receiverType && { receiverType }),
|
|
1280
1514
|
enclosingFunction,
|
|
1281
|
-
uncertain
|
|
1515
|
+
uncertain,
|
|
1516
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
1517
|
+
argCount
|
|
1282
1518
|
});
|
|
1283
1519
|
}
|
|
1284
1520
|
}
|
|
@@ -2165,12 +2401,32 @@ const _JS_LIFECYCLE_METHODS = new Set([
|
|
|
2165
2401
|
'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
|
|
2166
2402
|
]);
|
|
2167
2403
|
|
|
2404
|
+
/**
|
|
2405
|
+
* Classify a JS/TS symbol as a runtime entry point of a specific kind.
|
|
2406
|
+
* Returns 'framework' | null.
|
|
2407
|
+
*
|
|
2408
|
+
* - 'framework': React lifecycle methods (componentDidMount, etc.) and Web
|
|
2409
|
+
* Components callbacks (connectedCallback, etc.) — invoked by
|
|
2410
|
+
* the framework, not user code.
|
|
2411
|
+
*
|
|
2412
|
+
* Note: in JS/TS, test cases are framework calls (`it`, `test`, `describe`)
|
|
2413
|
+
* not function definitions, so they aren't classified as test entry points
|
|
2414
|
+
* here — `_addAffectedTestCases` in core/tracing.js handles them via call
|
|
2415
|
+
* detection rather than this predicate.
|
|
2416
|
+
*
|
|
2417
|
+
* Used by tracing/search so `affectedTests` only tags genuine test cases.
|
|
2418
|
+
*/
|
|
2419
|
+
function getEntryPointKind(symbol) {
|
|
2420
|
+
if (symbol.isMethod && _JS_LIFECYCLE_METHODS.has(symbol.name)) return 'framework';
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2168
2424
|
/**
|
|
2169
2425
|
* Check if a symbol is a JS/TS-convention entry point.
|
|
2170
2426
|
* These are framework lifecycle methods invoked by React or Web Components.
|
|
2171
2427
|
*/
|
|
2172
2428
|
function isEntryPoint(symbol) {
|
|
2173
|
-
return
|
|
2429
|
+
return getEntryPointKind(symbol) !== null;
|
|
2174
2430
|
}
|
|
2175
2431
|
|
|
2176
2432
|
module.exports = {
|
|
@@ -2184,5 +2440,6 @@ module.exports = {
|
|
|
2184
2440
|
findExportsInCode,
|
|
2185
2441
|
findUsagesInCode,
|
|
2186
2442
|
isEntryPoint,
|
|
2443
|
+
getEntryPointKind,
|
|
2187
2444
|
parse
|
|
2188
2445
|
};
|