ucn 3.8.23 → 3.8.25

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.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +114 -11
  2. package/README.md +152 -156
  3. package/cli/index.js +363 -37
  4. package/core/analysis.js +936 -32
  5. package/core/bridge.js +1111 -0
  6. package/core/brief.js +408 -0
  7. package/core/cache.js +105 -5
  8. package/core/callers.js +72 -18
  9. package/core/check.js +200 -0
  10. package/core/discovery.js +57 -34
  11. package/core/entrypoints.js +638 -4
  12. package/core/execute.js +304 -5
  13. package/core/git-enrich.js +130 -0
  14. package/core/graph.js +24 -2
  15. package/core/output/analysis.js +157 -25
  16. package/core/output/brief.js +100 -0
  17. package/core/output/check.js +79 -0
  18. package/core/output/doctor.js +85 -0
  19. package/core/output/endpoints.js +239 -0
  20. package/core/output/extraction.js +2 -0
  21. package/core/output/find.js +126 -39
  22. package/core/output/graph.js +48 -15
  23. package/core/output/refactoring.js +103 -5
  24. package/core/output/reporting.js +63 -23
  25. package/core/output/search.js +110 -17
  26. package/core/output/shared.js +56 -2
  27. package/core/output.js +4 -0
  28. package/core/parser.js +8 -2
  29. package/core/project.js +39 -3
  30. package/core/registry.js +30 -14
  31. package/core/reporting.js +465 -2
  32. package/core/search.js +130 -10
  33. package/core/shared.js +101 -5
  34. package/core/tracing.js +16 -6
  35. package/core/verify.js +982 -95
  36. package/languages/go.js +91 -6
  37. package/languages/html.js +10 -0
  38. package/languages/java.js +151 -35
  39. package/languages/javascript.js +290 -33
  40. package/languages/python.js +78 -11
  41. package/languages/rust.js +267 -12
  42. package/languages/utils.js +315 -3
  43. package/mcp/server.js +91 -16
  44. 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
- const { name } = symbol;
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 for modifiers (avoid matching keywords in params)
67
- // Use the parameters node position to find the real paren (not annotation parens)
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 paramOffset = paramsNode ? paramsNode.startIndex - node.startIndex : -1;
71
- const preParams = paramOffset >= 0 ? text.substring(0, paramOffset) : text.split('\n').slice(0, 3).join(' ');
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
- if (child.type === 'constructor_declaration') {
605
- const nameNode = child.childForFieldName('name');
606
- const paramsNode = child.childForFieldName('parameters');
607
-
608
- if (nameNode) {
609
- const { startLine, endLine } = nodeToLocation(child, code);
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
- const m = symbol.modifiers || [];
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
  };