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/go.js
CHANGED
|
@@ -559,6 +559,32 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
559
559
|
const tree = parseTree(parser, code);
|
|
560
560
|
const calls = [];
|
|
561
561
|
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
562
|
+
|
|
563
|
+
// Helper: extract first string-arg literal from a call_expression node.
|
|
564
|
+
// Used by route extraction to capture path arg of http.HandleFunc("/p", h),
|
|
565
|
+
// r.GET("/users", listUsers), and detect fmt.Sprintf("/users/%d", id).
|
|
566
|
+
const { extractStringArg: _extractStringArg, extractSprintfPrefix: _extractSprintfPrefix } = require('./utils');
|
|
567
|
+
const getFirstStringArg = (callNode) => {
|
|
568
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
569
|
+
if (!argsNode) return null;
|
|
570
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
571
|
+
const arg = argsNode.namedChild(i);
|
|
572
|
+
if (arg.type === 'comment') continue;
|
|
573
|
+
// fmt.Sprintf interpolation
|
|
574
|
+
if (arg.type === 'call_expression') {
|
|
575
|
+
const inner = arg.childForFieldName('function');
|
|
576
|
+
if (inner?.type === 'selector_expression') {
|
|
577
|
+
const operand = inner.childForFieldName('operand');
|
|
578
|
+
const field = inner.childForFieldName('field');
|
|
579
|
+
if (operand?.text === 'fmt' && field && /^Sprintf$/.test(field.text)) {
|
|
580
|
+
return _extractSprintfPrefix(arg);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return _extractStringArg(arg);
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
};
|
|
562
588
|
// Skip common non-function identifiers when detecting callback arguments
|
|
563
589
|
const GO_SKIP_IDENTS = new Set(['nil', 'true', 'false', 'err', 'ctx', 'context', 'iota']);
|
|
564
590
|
// Track local closure names per function scope (scopeStartLine -> Set<name>)
|
|
@@ -846,12 +872,14 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
846
872
|
if (isFuncTypedParam(callName)) return true;
|
|
847
873
|
|
|
848
874
|
// Direct call: foo()
|
|
875
|
+
const firstArg = getFirstStringArg(node);
|
|
849
876
|
calls.push({
|
|
850
877
|
name: callName,
|
|
851
878
|
line: node.startPosition.row + 1,
|
|
852
879
|
isMethod: false,
|
|
853
880
|
enclosingFunction,
|
|
854
|
-
uncertain
|
|
881
|
+
uncertain,
|
|
882
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
855
883
|
});
|
|
856
884
|
} else if (funcNode.type === 'selector_expression') {
|
|
857
885
|
// Method or package call: obj.Method() or pkg.Func()
|
|
@@ -864,6 +892,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
864
892
|
// If receiver is a known import alias, this is a package call, not a method call
|
|
865
893
|
const isPkgCall = receiver && importAliases.has(receiver);
|
|
866
894
|
const receiverType = (!isPkgCall && receiver) ? getReceiverType(receiver) : undefined;
|
|
895
|
+
const firstArg = getFirstStringArg(node);
|
|
867
896
|
calls.push({
|
|
868
897
|
name: fieldNode.text,
|
|
869
898
|
line: node.startPosition.row + 1,
|
|
@@ -871,13 +900,55 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
871
900
|
receiver,
|
|
872
901
|
...(receiverType && { receiverType }),
|
|
873
902
|
enclosingFunction,
|
|
874
|
-
uncertain
|
|
903
|
+
uncertain,
|
|
904
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
875
905
|
});
|
|
876
906
|
}
|
|
877
907
|
}
|
|
878
908
|
return true;
|
|
879
909
|
}
|
|
880
910
|
|
|
911
|
+
// R3-NEW-3: Detect Go struct composite literals as constructor calls.
|
|
912
|
+
// Foo{x: 1} → call(name='Foo', isConstructor:true)
|
|
913
|
+
// pkg.Foo{...} → call(name='Foo', isConstructor:true) — strip package
|
|
914
|
+
// &Foo{...} → composite_literal nested inside unary_expression;
|
|
915
|
+
// handled because we visit the inner composite_literal node.
|
|
916
|
+
// Skipped: anonymous types (slices/maps/arrays/struct types):
|
|
917
|
+
// []int{...}, map[string]int{...}, struct{...}{...}, [3]int{...}
|
|
918
|
+
// These have non-identifier type children — only type_identifier and
|
|
919
|
+
// qualified_type produce a real type name.
|
|
920
|
+
if (node.type === 'composite_literal') {
|
|
921
|
+
// Skip composite literals that are nested inside another composite_literal's
|
|
922
|
+
// value position — those are inner field initializers like
|
|
923
|
+
// `Outer{ field: Inner{...} }`. Both the outer and inner are real
|
|
924
|
+
// constructors, so we DO emit each, but we must not emit the same
|
|
925
|
+
// node twice. Tree-sitter visits each node once, so this is fine.
|
|
926
|
+
const typeNode = node.childForFieldName('type');
|
|
927
|
+
if (typeNode) {
|
|
928
|
+
let typeName = null;
|
|
929
|
+
if (typeNode.type === 'type_identifier') {
|
|
930
|
+
// Foo{...}
|
|
931
|
+
typeName = typeNode.text;
|
|
932
|
+
} else if (typeNode.type === 'qualified_type') {
|
|
933
|
+
// pkg.Foo{...}
|
|
934
|
+
const tn = typeNode.childForFieldName('name');
|
|
935
|
+
if (tn) typeName = tn.text;
|
|
936
|
+
}
|
|
937
|
+
// Skip anonymous types (slice_type, map_type, array_type, struct_type, etc.)
|
|
938
|
+
if (typeName) {
|
|
939
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
940
|
+
calls.push({
|
|
941
|
+
name: typeName,
|
|
942
|
+
line: node.startPosition.row + 1,
|
|
943
|
+
isMethod: false,
|
|
944
|
+
isConstructor: true,
|
|
945
|
+
enclosingFunction,
|
|
946
|
+
uncertain: false
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
881
952
|
// Detect function references passed as arguments: dc.worker passed to UntilWithContext(ctx, dc.worker, ...)
|
|
882
953
|
// selector_expression inside argument_list (not inside call_expression as the function)
|
|
883
954
|
if (node.type === 'selector_expression' && node.parent?.type === 'argument_list') {
|
|
@@ -1352,15 +1423,28 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1352
1423
|
return usages;
|
|
1353
1424
|
}
|
|
1354
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* Classify a Go symbol as a runtime entry point of a specific kind.
|
|
1428
|
+
* Returns 'test' | 'main' | null.
|
|
1429
|
+
*
|
|
1430
|
+
* - 'test': functions named Test*, Benchmark*, Example*, Fuzz* — invoked by `go test`.
|
|
1431
|
+
* - 'main': fn main / fn init — invoked by the Go runtime.
|
|
1432
|
+
*
|
|
1433
|
+
* Used by tracing/search so `affectedTests` only tags genuine test functions.
|
|
1434
|
+
*/
|
|
1435
|
+
function getEntryPointKind(symbol) {
|
|
1436
|
+
const { name } = symbol;
|
|
1437
|
+
if (/^(Test|Benchmark|Example|Fuzz)[A-Z_]/.test(name)) return 'test';
|
|
1438
|
+
if (name === 'main' || name === 'init') return 'main';
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1355
1442
|
/**
|
|
1356
1443
|
* Check if a symbol is a Go-convention entry point.
|
|
1357
1444
|
* These are invoked by the Go runtime or test runner, not user code.
|
|
1358
1445
|
*/
|
|
1359
1446
|
function isEntryPoint(symbol) {
|
|
1360
|
-
|
|
1361
|
-
if (name === 'main' || name === 'init') return true;
|
|
1362
|
-
if (/^(Test|Benchmark|Example|Fuzz)[A-Z_]/.test(name)) return true;
|
|
1363
|
-
return false;
|
|
1447
|
+
return getEntryPointKind(symbol) !== null;
|
|
1364
1448
|
}
|
|
1365
1449
|
|
|
1366
1450
|
module.exports = {
|
|
@@ -1372,5 +1456,6 @@ module.exports = {
|
|
|
1372
1456
|
findExportsInCode,
|
|
1373
1457
|
findUsagesInCode,
|
|
1374
1458
|
isEntryPoint,
|
|
1459
|
+
getEntryPointKind,
|
|
1375
1460
|
parse
|
|
1376
1461
|
};
|
package/languages/html.js
CHANGED
|
@@ -311,6 +311,15 @@ function findUsagesInCode(code, name, parser) {
|
|
|
311
311
|
return jsUsages.concat(handlerUsages);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Classify an HTML symbol as a runtime entry point.
|
|
316
|
+
* HTML has no entry points of its own — JS extracted from <script> blocks
|
|
317
|
+
* is classified by the JS predicate.
|
|
318
|
+
*/
|
|
319
|
+
function getEntryPointKind() {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
314
323
|
/**
|
|
315
324
|
* HTML has no entry points of its own.
|
|
316
325
|
*/
|
|
@@ -330,6 +339,7 @@ module.exports = {
|
|
|
330
339
|
findExportsInCode,
|
|
331
340
|
findUsagesInCode,
|
|
332
341
|
isEntryPoint,
|
|
342
|
+
getEntryPointKind,
|
|
333
343
|
// Exported for testing
|
|
334
344
|
extractScriptBlocks,
|
|
335
345
|
buildVirtualJSContent,
|
package/languages/java.js
CHANGED
|
@@ -63,12 +63,24 @@ function extractModifiers(node) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// Also check text before parameter list
|
|
67
|
-
//
|
|
66
|
+
// Also check text before the parameter list (methods) or the class body
|
|
67
|
+
// opening brace (classes/interfaces). Without this scope, the fallback
|
|
68
|
+
// would scan into field declarations and leak `private`/`final` from the
|
|
69
|
+
// body up onto the class signature.
|
|
68
70
|
const text = node.text;
|
|
69
71
|
const paramsNode = node.childForFieldName('parameters');
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
const bodyNode = node.childForFieldName('body');
|
|
73
|
+
let preParams;
|
|
74
|
+
if (paramsNode) {
|
|
75
|
+
preParams = text.substring(0, paramsNode.startIndex - node.startIndex);
|
|
76
|
+
} else if (bodyNode) {
|
|
77
|
+
preParams = text.substring(0, bodyNode.startIndex - node.startIndex);
|
|
78
|
+
} else {
|
|
79
|
+
// Last-resort fallback: only the first line. Class bodies start on
|
|
80
|
+
// their own line nearly always, so this avoids leaking field modifiers.
|
|
81
|
+
const firstLine = text.split('\n')[0] || '';
|
|
82
|
+
preParams = firstLine;
|
|
83
|
+
}
|
|
72
84
|
const keywords = ['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default'];
|
|
73
85
|
for (const kw of keywords) {
|
|
74
86
|
if (preParams.includes(kw + ' ') && !modifiers.includes(kw)) {
|
|
@@ -98,6 +110,80 @@ function extractAnnotations(node) {
|
|
|
98
110
|
return annotations;
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Extract annotations along with their string-literal first argument.
|
|
115
|
+
* Returns array of { name, args: string|null, firstStringArg: string|null }.
|
|
116
|
+
* @GetMapping("/users/{id}") → { name: 'GetMapping', args: '"/users/{id}"', firstStringArg: '/users/{id}' }
|
|
117
|
+
* @Override → { name: 'Override', args: null, firstStringArg: null }
|
|
118
|
+
* @RequestMapping(value = "/api", method = RequestMethod.GET)
|
|
119
|
+
* → { name: 'RequestMapping', args: 'value = "/api", method = RequestMethod.GET',
|
|
120
|
+
* firstStringArg: '/api' }
|
|
121
|
+
*
|
|
122
|
+
* @param {Node} node - Method/class node
|
|
123
|
+
* @returns {Array<{name: string, args: string|null, firstStringArg: string|null}>}
|
|
124
|
+
*/
|
|
125
|
+
function extractAnnotationsWithArgs(node) {
|
|
126
|
+
const result = [];
|
|
127
|
+
const modifiersNode = node.childForFieldName('modifiers') || (() => {
|
|
128
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
129
|
+
if (node.namedChild(i).type === 'modifiers') return node.namedChild(i);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
})();
|
|
133
|
+
if (!modifiersNode) return result;
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < modifiersNode.namedChildCount; i++) {
|
|
136
|
+
const mod = modifiersNode.namedChild(i);
|
|
137
|
+
if (mod.type === 'marker_annotation') {
|
|
138
|
+
// @Override (no args)
|
|
139
|
+
const nameNode = mod.childForFieldName('name');
|
|
140
|
+
if (nameNode) {
|
|
141
|
+
result.push({ name: nameNode.text, args: null, firstStringArg: null });
|
|
142
|
+
}
|
|
143
|
+
} else if (mod.type === 'annotation') {
|
|
144
|
+
const nameNode = mod.childForFieldName('name');
|
|
145
|
+
const argsNode = mod.childForFieldName('arguments');
|
|
146
|
+
const name = nameNode ? nameNode.text : null;
|
|
147
|
+
const argsRaw = argsNode ? argsNode.text.replace(/^\(|\)$/g, '') : null;
|
|
148
|
+
// Find first string-literal arg (handles positional and value=... patterns)
|
|
149
|
+
let firstStringArg = null;
|
|
150
|
+
if (argsNode) {
|
|
151
|
+
// Walk children: positional string_literal OR element_value_pair with key 'value'
|
|
152
|
+
for (let j = 0; j < argsNode.namedChildCount; j++) {
|
|
153
|
+
const child = argsNode.namedChild(j);
|
|
154
|
+
if (child.type === 'string_literal') {
|
|
155
|
+
firstStringArg = stripJavaString(child.text);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
if (child.type === 'element_value_pair') {
|
|
159
|
+
const key = child.childForFieldName('key');
|
|
160
|
+
const value = child.childForFieldName('value');
|
|
161
|
+
if (key?.text === 'value' && value?.type === 'string_literal') {
|
|
162
|
+
firstStringArg = stripJavaString(value.text);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Fallback: first string_literal anywhere in subtree (handles path = "/x")
|
|
168
|
+
if (!firstStringArg) {
|
|
169
|
+
const m = argsNode.text.match(/"([^"\\]|\\.)*"/);
|
|
170
|
+
if (m) firstStringArg = m[0].slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (name) {
|
|
174
|
+
result.push({ name, args: argsRaw, firstStringArg });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stripJavaString(text) {
|
|
182
|
+
if (!text) return text;
|
|
183
|
+
if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
|
|
184
|
+
return text;
|
|
185
|
+
}
|
|
186
|
+
|
|
101
187
|
/**
|
|
102
188
|
* Extract return type from method
|
|
103
189
|
*/
|
|
@@ -144,6 +230,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
144
230
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
145
231
|
const modifiers = extractModifiers(node);
|
|
146
232
|
const annotations = extractAnnotations(node);
|
|
233
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
147
234
|
const returnType = extractReturnType(node);
|
|
148
235
|
const generics = extractGenerics(node);
|
|
149
236
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
@@ -162,6 +249,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
162
249
|
...(generics && { generics }),
|
|
163
250
|
...(docstring && { docstring }),
|
|
164
251
|
...(annotations.length > 0 && { annotations }),
|
|
252
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
165
253
|
...(nameLine !== startLine && { nameLine })
|
|
166
254
|
});
|
|
167
255
|
}
|
|
@@ -187,6 +275,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
187
275
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
188
276
|
const modifiers = extractModifiers(node);
|
|
189
277
|
const annotations = extractAnnotations(node);
|
|
278
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
190
279
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
191
280
|
const nameLine = nameNode.startPosition.row + 1;
|
|
192
281
|
|
|
@@ -245,6 +334,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
245
334
|
const members = extractClassMembers(node, lines);
|
|
246
335
|
const modifiers = extractModifiers(node);
|
|
247
336
|
const annotations = extractAnnotations(node);
|
|
337
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
248
338
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
249
339
|
const generics = extractGenerics(node);
|
|
250
340
|
const extendsInfo = extractExtends(node);
|
|
@@ -265,6 +355,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
265
355
|
...(docstring && { docstring }),
|
|
266
356
|
...(generics && { generics }),
|
|
267
357
|
...(annotations.length > 0 && { annotations }),
|
|
358
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
268
359
|
...(extendsInfo && { extends: extendsInfo }),
|
|
269
360
|
...(implementsInfo.length > 0 && { implements: implementsInfo })
|
|
270
361
|
});
|
|
@@ -283,6 +374,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
283
374
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
284
375
|
const modifiers = extractModifiers(node);
|
|
285
376
|
const annotations = extractAnnotations(node);
|
|
377
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
286
378
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
287
379
|
const generics = extractGenerics(node);
|
|
288
380
|
const extendsInfo = extractInterfaceExtends(node);
|
|
@@ -297,6 +389,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
297
389
|
...(docstring && { docstring }),
|
|
298
390
|
...(generics && { generics }),
|
|
299
391
|
...(annotations.length > 0 && { annotations }),
|
|
392
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
300
393
|
...(extendsInfo.length > 0 && { extends: extendsInfo.join(', ') })
|
|
301
394
|
});
|
|
302
395
|
}
|
|
@@ -314,6 +407,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
314
407
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
315
408
|
const modifiers = extractModifiers(node);
|
|
316
409
|
const annotations = extractAnnotations(node);
|
|
410
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
317
411
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
318
412
|
|
|
319
413
|
classes.push({
|
|
@@ -341,6 +435,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
341
435
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
342
436
|
const modifiers = extractModifiers(node);
|
|
343
437
|
const annotations = extractAnnotations(node);
|
|
438
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(node);
|
|
344
439
|
const docstring = extractJavaDocstring(lines, startLine);
|
|
345
440
|
const generics = extractGenerics(node);
|
|
346
441
|
const implementsInfo = extractImplements(node);
|
|
@@ -566,6 +661,7 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
566
661
|
if (nameNode) {
|
|
567
662
|
const { startLine, endLine } = nodeToLocation(child, code);
|
|
568
663
|
const modifiers = extractModifiers(child);
|
|
664
|
+
const annotationsWithArgs = extractAnnotationsWithArgs(child);
|
|
569
665
|
// Interface methods are implicitly public and abstract in Java
|
|
570
666
|
if (isInterface) {
|
|
571
667
|
if (!modifiers.includes('public')) modifiers.push('public');
|
|
@@ -595,36 +691,19 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
595
691
|
isMethod: true, // Mark as method for context() lookups
|
|
596
692
|
...(returnType && { returnType }),
|
|
597
693
|
...(docstring && { docstring }),
|
|
694
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
598
695
|
...(nameLine !== startLine && { nameLine })
|
|
599
696
|
});
|
|
600
697
|
}
|
|
601
698
|
}
|
|
602
699
|
|
|
603
|
-
// Constructor declarations
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const modifiers = extractModifiers(child);
|
|
611
|
-
const docstring = extractJavaDocstring(code, startLine);
|
|
612
|
-
const nameLine = nameNode.startPosition.row + 1;
|
|
613
|
-
|
|
614
|
-
members.push({
|
|
615
|
-
name: nameNode.text,
|
|
616
|
-
params: extractJavaParams(paramsNode),
|
|
617
|
-
paramsStructured: parseStructuredParams(paramsNode, 'java'),
|
|
618
|
-
startLine,
|
|
619
|
-
endLine,
|
|
620
|
-
memberType: 'constructor',
|
|
621
|
-
modifiers,
|
|
622
|
-
isMethod: true, // Mark as method for context() lookups
|
|
623
|
-
...(docstring && { docstring }),
|
|
624
|
-
...(nameLine !== startLine && { nameLine })
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
}
|
|
700
|
+
// Constructor declarations: intentionally NOT emitted as separate class
|
|
701
|
+
// members. The class itself is the symbol; `new Foo(...)` calls resolve
|
|
702
|
+
// to the class via `isConstructor: true` on the call. Emitting the
|
|
703
|
+
// constructor as a member would create duplicate `find Foo` results
|
|
704
|
+
// (one for class, one for constructor), forcing users to disambiguate.
|
|
705
|
+
// Constructor signature info (params, line) remains accessible by reading
|
|
706
|
+
// the class body when needed (e.g. via verify's AST walk).
|
|
628
707
|
}
|
|
629
708
|
|
|
630
709
|
return members;
|
|
@@ -726,6 +805,20 @@ function findCallsInCode(code, parser) {
|
|
|
726
805
|
// Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
|
|
727
806
|
const scopeTypes = new Map();
|
|
728
807
|
|
|
808
|
+
// Helper: extract first string-arg literal from a method_invocation node.
|
|
809
|
+
// Used by route extraction to capture path arg of webClient.uri("/users") etc.
|
|
810
|
+
const { extractStringArg: _extractStringArg } = require('./utils');
|
|
811
|
+
const getFirstStringArg = (callNode) => {
|
|
812
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
813
|
+
if (!argsNode) return null;
|
|
814
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
815
|
+
const arg = argsNode.namedChild(i);
|
|
816
|
+
if (arg.type === 'comment') continue;
|
|
817
|
+
return _extractStringArg(arg);
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
};
|
|
821
|
+
|
|
729
822
|
// Helper to check if a node creates a function scope
|
|
730
823
|
const isFunctionNode = (node) => {
|
|
731
824
|
return ['method_declaration', 'constructor_declaration', 'lambda_expression'].includes(node.type);
|
|
@@ -827,13 +920,15 @@ function findCallsInCode(code, parser) {
|
|
|
827
920
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
828
921
|
const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
|
|
829
922
|
const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
|
|
923
|
+
const firstArg = getFirstStringArg(node);
|
|
830
924
|
calls.push({
|
|
831
925
|
name: nameNode.text,
|
|
832
926
|
line: node.startPosition.row + 1,
|
|
833
927
|
isMethod: !!objNode,
|
|
834
928
|
receiver,
|
|
835
929
|
...(receiverType && { receiverType }),
|
|
836
|
-
enclosingFunction
|
|
930
|
+
enclosingFunction,
|
|
931
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
837
932
|
});
|
|
838
933
|
}
|
|
839
934
|
return true;
|
|
@@ -1173,16 +1268,36 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1173
1268
|
return usages;
|
|
1174
1269
|
}
|
|
1175
1270
|
|
|
1271
|
+
/**
|
|
1272
|
+
* Classify a Java symbol as a runtime entry point of a specific kind.
|
|
1273
|
+
* Returns 'test' | 'main' | 'framework' | null.
|
|
1274
|
+
*
|
|
1275
|
+
* - 'test': JUnit @Test family (Test, ParameterizedTest, RepeatedTest,
|
|
1276
|
+
* TestFactory, TestTemplate) and JUnit lifecycle hooks
|
|
1277
|
+
* (BeforeEach, AfterEach, BeforeAll, AfterAll).
|
|
1278
|
+
* - 'main': public static void main() — invoked by the JVM.
|
|
1279
|
+
* - 'framework': @Override methods (invoked by the type-system contract).
|
|
1280
|
+
*
|
|
1281
|
+
* Used by tracing/search so `affectedTests` only tags genuine test methods.
|
|
1282
|
+
*/
|
|
1283
|
+
function getEntryPointKind(symbol) {
|
|
1284
|
+
const m = symbol.modifiers || [];
|
|
1285
|
+
// JUnit @Test family — full lowercase set so deadcode/test detection treats
|
|
1286
|
+
// ParameterizedTest, RepeatedTest, TestFactory, TestTemplate as test entry points.
|
|
1287
|
+
const TEST_ANNOTATIONS = ['test', 'parameterizedtest', 'repeatedtest', 'testfactory', 'testtemplate',
|
|
1288
|
+
'beforeeach', 'aftereach', 'beforeall', 'afterall', 'before', 'after'];
|
|
1289
|
+
if (m.some(x => TEST_ANNOTATIONS.includes(x))) return 'test';
|
|
1290
|
+
if (symbol.name === 'main' && m.includes('public') && m.includes('static')) return 'main';
|
|
1291
|
+
if (m.includes('override')) return 'framework';
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1176
1295
|
/**
|
|
1177
1296
|
* Check if a symbol is a Java-convention entry point.
|
|
1178
1297
|
* These are invoked by the JVM runtime, test runners, or required by type system.
|
|
1179
1298
|
*/
|
|
1180
1299
|
function isEntryPoint(symbol) {
|
|
1181
|
-
|
|
1182
|
-
if (symbol.name === 'main' && m.includes('public') && m.includes('static')) return true;
|
|
1183
|
-
if (m.includes('test')) return true;
|
|
1184
|
-
if (m.includes('override')) return true;
|
|
1185
|
-
return false;
|
|
1300
|
+
return getEntryPointKind(symbol) !== null;
|
|
1186
1301
|
}
|
|
1187
1302
|
|
|
1188
1303
|
module.exports = {
|
|
@@ -1194,5 +1309,6 @@ module.exports = {
|
|
|
1194
1309
|
findExportsInCode,
|
|
1195
1310
|
findUsagesInCode,
|
|
1196
1311
|
isEntryPoint,
|
|
1312
|
+
getEntryPointKind,
|
|
1197
1313
|
parse
|
|
1198
1314
|
};
|