gitnexus 1.6.9-rc.13 → 1.6.9-rc.15

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.
@@ -576,13 +576,16 @@ export const JAVA_HTTP_PLUGIN = {
576
576
  language: Java,
577
577
  // routeCoverage intentionally LEFT at the default 'partial' (#2138 Part 2).
578
578
  // The graph provider set is a strict *subset* of this scan()'s provider set —
579
- // ingestion does NOT emit a Route node for (1) array-form `@GetMapping({...})`,
580
- // (2) interface-inherited Spring routes, or (3) the 2nd verb of a same-URL
581
- // GET+POST pair (Route nodes are URL-keyed). Declaring 'complete' here would
582
- // let the parse-skip drop those group-only providers. Java flips to 'complete'
583
- // only once ingestion provider extraction matches this scan (a follow-up:
584
- // array-form query branch + interface-inheritance emission + per-verb Route
585
- // identity). `hasConsumerSignals` below is kept ready for that flip.
579
+ // ingestion does NOT emit a Route node for (1) a method-level array route
580
+ // nested under a class-level array-form `@RequestMapping` (ingestion suppresses
581
+ // it rather than drop the prefix; bare/scalar-prefixed array methods ARE now
582
+ // emitted see #2280), (2) interface-inherited Spring routes, or (3) the 2nd
583
+ // verb of a same-URL GET+POST pair (Route nodes are URL-keyed). Declaring
584
+ // 'complete' here would let the parse-skip drop those group-only providers.
585
+ // Java flips to 'complete' only once ingestion provider extraction matches this
586
+ // scan (a follow-up: class-level array-form prefix support + interface-
587
+ // inheritance emission + per-verb Route identity — tracked in #2280).
588
+ // `hasConsumerSignals` below is kept ready for that flip.
586
589
  // Consumer signals this plugin's scan() can detect: RestTemplate / WebClient /
587
590
  // OkHttp / Java-HttpClient / Apache-HttpClient call sites, OpenFeign
588
591
  // (`@FeignClient` + `@RequestLine`) interfaces, and Spring 6 HTTP Interface
@@ -27,6 +27,7 @@ const FUNCTION_DECLARATION_TYPES = new Set([
27
27
  'generator_function_declaration',
28
28
  'function_item',
29
29
  ]);
30
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
30
31
  import { createFieldExtractor } from '../field-extractors/generic.js';
31
32
  import { cConfig as cFieldConfig, cppConfig as cppFieldConfig, } from '../field-extractors/configs/c-cpp.js';
32
33
  import { createMethodExtractor } from '../method-extractors/generic.js';
@@ -348,6 +349,8 @@ export const cProvider = defineLanguage({
348
349
  }),
349
350
  variableExtractor: createVariableExtractor(cVariableConfig),
350
351
  classExtractor: cClassExtractor,
352
+ // ── Doxygen doc comment → description (issue #2270) ──
353
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
351
354
  labelOverride: cppLabelOverride,
352
355
  builtInNames: C_BUILT_INS,
353
356
  // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
@@ -430,6 +433,8 @@ export const cppProvider = defineLanguage({
430
433
  }),
431
434
  variableExtractor: createVariableExtractor(cppVariableConfig),
432
435
  classExtractor: cppClassExtractor,
436
+ // ── Doxygen doc comment → description (issue #2270) ──
437
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
433
438
  labelOverride: cppLabelOverride,
434
439
  builtInNames: C_BUILT_INS,
435
440
  extractTemplateConstraints: extractCppTemplateConstraintsForProvider,
@@ -14,6 +14,7 @@ import { csharpExportChecker } from '../export-detection.js';
14
14
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
15
15
  import { csharpImportConfig } from '../import-resolvers/configs/csharp.js';
16
16
  import { CSHARP_QUERIES } from '../tree-sitter-queries.js';
17
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
17
18
  import { createCallExtractor } from '../call-extractors/generic.js';
18
19
  import { csharpCallConfig } from '../call-extractors/configs/csharp.js';
19
20
  import { createFieldExtractor } from '../field-extractors/generic.js';
@@ -180,6 +181,8 @@ export const csharpProvider = defineLanguage({
180
181
  methodExtractor: createMethodExtractor(csharpMethodConfig),
181
182
  variableExtractor: createVariableExtractor(csharpVariableConfig),
182
183
  classExtractor: createClassExtractor(csharpClassConfig),
184
+ // ── XML doc comments (`///`) → description (issue #2270) ──
185
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
183
186
  builtInNames: BUILT_INS,
184
187
  // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
185
188
  // C# is the second migration after Python. See ./csharp/index.ts for
@@ -8,7 +8,7 @@
8
8
  * as a sibling of function_signature/method_signature (not as a child).
9
9
  * The hook resolves the enclosing function by inspecting the previous sibling.
10
10
  */
11
- import { FUNCTION_NODE_TYPES } from '../utils/ast-helpers.js';
11
+ import { createLeadingDocDescriptionExtractor, FUNCTION_NODE_TYPES, } from '../utils/ast-helpers.js';
12
12
  import { SupportedLanguages } from '../../../_shared/index.js';
13
13
  import { createClassExtractor } from '../class-extractors/generic.js';
14
14
  import { dartClassConfig } from '../class-extractors/configs/dart.js';
@@ -108,6 +108,8 @@ export const dartProvider = defineLanguage({
108
108
  methodExtractor: createMethodExtractor(dartMethodConfig),
109
109
  variableExtractor: createVariableExtractor(dartVariableConfig),
110
110
  classExtractor: createClassExtractor(dartClassConfig),
111
+ // ── Dartdoc (`///`) → description (issue #2270) ──
112
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
111
113
  enclosingFunctionFinder: dartEnclosingFunctionFinder,
112
114
  builtInNames: DART_BUILT_INS,
113
115
  // ── Scope-based resolution hooks (RFC #909 Ring 3, issue #939) ──────────────
@@ -10,6 +10,7 @@
10
10
  import { SupportedLanguages } from '../../../_shared/index.js';
11
11
  import { createClassExtractor } from '../class-extractors/generic.js';
12
12
  import { goClassConfig } from '../class-extractors/configs/go.js';
13
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
13
14
  import { createGoCfgVisitor } from '../cfg/visitors/go.js';
14
15
  import { defineLanguage } from '../language-provider.js';
15
16
  import { typeConfig as goConfig } from '../type-extractors/go.js';
@@ -126,6 +127,12 @@ export const goProvider = defineLanguage({
126
127
  methodExtractor: createMethodExtractor(goMethodConfig),
127
128
  variableExtractor: createVariableExtractor(goVariableConfig),
128
129
  classExtractor: createClassExtractor(goClassConfig),
130
+ // ── godoc (`//` leading comments) → description (issue #2270). Build/tool
131
+ // directives (//go:…, // +build, //nolint, //line) are not documentation. ──
132
+ descriptionExtractor: createLeadingDocDescriptionExtractor({
133
+ lineCommentPrefixes: ['//'],
134
+ lineDirectivePrefixes: ['//go:', '// +build', '//nolint', '//line'],
135
+ }),
129
136
  builtInNames: GO_BUILT_INS,
130
137
  // ── RFC #909 Ring 3: scope-based resolution hooks ──────────
131
138
  emitScopeCaptures: emitGoScopeCaptures,
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { javaClassConfig } from '../class-extractors/configs/jvm.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
13
14
  import { javaTypeConfig } from '../type-extractors/jvm.js';
14
15
  import { extractSpringRoutes } from '../route-extractors/spring.js';
15
16
  import { javaExportChecker } from '../export-detection.js';
@@ -94,6 +95,8 @@ export const javaProvider = defineLanguage({
94
95
  methodExtractor: createMethodExtractor(javaMethodConfig),
95
96
  variableExtractor: createVariableExtractor(javaVariableConfig),
96
97
  classExtractor: createClassExtractor(javaClassConfig),
98
+ // ── Javadoc → description (issue #2270) ──
99
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
97
100
  // ── RFC #909 Ring 3: scope-based resolution hooks ──
98
101
  emitScopeCaptures: emitJavaScopeCaptures,
99
102
  // ── PDG: per-function CFG + def/use harvest (#2195 U4) ──
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { kotlinClassConfig } from '../class-extractors/configs/jvm.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
13
14
  import { assertCloneable } from '../workers/clone-safety.js';
14
15
  import { kotlinTypeConfig } from '../type-extractors/jvm.js';
15
16
  import { kotlinExportChecker } from '../export-detection.js';
@@ -153,6 +154,8 @@ export const kotlinProvider = defineLanguage({
153
154
  variableExtractor: createVariableExtractor(kotlinVariableConfig),
154
155
  classExtractor: createClassExtractor(kotlinClassConfig),
155
156
  builtInNames: BUILT_INS,
157
+ // ── KDoc → description (issue #2270) ──
158
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
156
159
  labelOverride: (functionNode, defaultLabel) => {
157
160
  if (defaultLabel !== 'Function')
158
161
  return defaultLabel;
@@ -10,13 +10,13 @@ import { SupportedLanguages } from '../../../_shared/index.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { phpClassConfig } from '../class-extractors/configs/php.js';
12
12
  import { createPhpCfgVisitor } from '../cfg/visitors/php.js';
13
- import { defineLanguage } from '../language-provider.js';
13
+ import { defineLanguage, } from '../language-provider.js';
14
14
  import { typeConfig as phpConfig } from '../type-extractors/php.js';
15
15
  import { phpExportChecker } from '../export-detection.js';
16
16
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
17
17
  import { phpImportConfig } from '../import-resolvers/configs/php.js';
18
18
  import { PHP_QUERIES } from '../tree-sitter-queries.js';
19
- import { findDescendant, extractStringContent } from '../utils/ast-helpers.js';
19
+ import { findDescendant, extractStringContent, createLeadingDocDescriptionExtractor, } from '../utils/ast-helpers.js';
20
20
  import { createFieldExtractor } from '../field-extractors/generic.js';
21
21
  import { phpConfig as phpFieldConfig } from '../field-extractors/configs/php.js';
22
22
  import { createMethodExtractor } from '../method-extractors/generic.js';
@@ -203,18 +203,29 @@ function extractEloquentRelationDescription(methodNode) {
203
203
  return relType;
204
204
  return null;
205
205
  }
206
+ /** PHPDoc-docblock fallback, shared with the other leading-comment languages. */
207
+ const phpLeadingDocFallback = createLeadingDocDescriptionExtractor();
206
208
  /**
207
209
  * LanguageProvider.descriptionExtractor implementation for PHP.
208
- * Extracts Eloquent model property metadata and relationship descriptions.
210
+ * Eloquent model property metadata and relationship descriptions take
211
+ * precedence (they are richer than prose); otherwise documentable symbols fall
212
+ * back to their leading PHPDoc docblock (issue #2270), mirroring the other
213
+ * leading-comment languages.
209
214
  */
210
215
  function phpDescriptionExtractor(nodeLabel, nodeName, captureMap) {
211
- if (nodeLabel === 'Property' && captureMap['definition.property']) {
212
- return extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
216
+ const propertyNode = captureMap['definition.property'];
217
+ if (nodeLabel === 'Property' && propertyNode) {
218
+ const eloquentProperty = extractPhpPropertyDescription(nodeName, propertyNode);
219
+ if (eloquentProperty)
220
+ return eloquentProperty;
213
221
  }
214
- if (nodeLabel === 'Method' && captureMap['definition.method']) {
215
- return extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
222
+ const methodNode = captureMap['definition.method'];
223
+ if (nodeLabel === 'Method' && methodNode) {
224
+ const eloquentRelation = extractEloquentRelationDescription(methodNode);
225
+ if (eloquentRelation)
226
+ return eloquentRelation;
216
227
  }
217
- return undefined;
228
+ return phpLeadingDocFallback(nodeLabel, nodeName, captureMap);
218
229
  }
219
230
  /** Detect Laravel route files by path convention. */
220
231
  function isPhpRouteFile(filePath) {
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { rubyClassConfig } from '../class-extractors/configs/ruby.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
13
14
  import { typeConfig as rubyConfig } from '../type-extractors/ruby.js';
14
15
  import { routeRubyCall } from '../call-routing.js';
15
16
  import { rubyExportChecker } from '../export-detection.js';
@@ -179,6 +180,20 @@ export const rubyProvider = defineLanguage({
179
180
  }),
180
181
  variableExtractor: createVariableExtractor(rubyVariableConfig),
181
182
  classExtractor: createClassExtractor(rubyClassConfig),
183
+ // ── Leading `#` comments (RDoc/YARD) → description (issue #2270). Magic
184
+ // comments and the shebang are not documentation. ──
185
+ descriptionExtractor: createLeadingDocDescriptionExtractor({
186
+ lineCommentPrefixes: ['#'],
187
+ lineDirectivePrefixes: [
188
+ '# frozen_string_literal:',
189
+ '# encoding:',
190
+ '# coding:',
191
+ '# -*-',
192
+ '#!',
193
+ '# rubocop:',
194
+ '# typed:',
195
+ ],
196
+ }),
182
197
  labelOverride: rubyLabelOverride,
183
198
  // Ruby MRO is kind-aware: prepend providers beat the class's own method,
184
199
  // which in turn beats include providers. The graph-level MRO phase
@@ -11,6 +11,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
11
11
  import { createClassExtractor } from '../class-extractors/generic.js';
12
12
  import { rustClassConfig } from '../class-extractors/configs/rust.js';
13
13
  import { defineLanguage } from '../language-provider.js';
14
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
14
15
  import { typeConfig as rustConfig } from '../type-extractors/rust.js';
15
16
  import { rustExportChecker } from '../export-detection.js';
16
17
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
@@ -159,6 +160,13 @@ export const rustProvider = defineLanguage({
159
160
  }),
160
161
  variableExtractor: createVariableExtractor(rustVariableConfig),
161
162
  classExtractor: createClassExtractor(rustClassConfig),
163
+ // ── Rust outer doc comments (`///`, `/** */`) → description (issue #2270).
164
+ // `//!` / `/*!` are INNER docs (document the enclosing item), so they must
165
+ // not attach to the following item — opt out of both. ──
166
+ descriptionExtractor: createLeadingDocDescriptionExtractor({
167
+ lineCommentPrefixes: ['///'],
168
+ blockDocPrefixes: ['/**'],
169
+ }),
162
170
  builtInNames: BUILT_INS,
163
171
  // ── RFC #909 Ring 3: scope-based resolution hooks ──────────
164
172
  emitScopeCaptures: emitRustScopeCaptures,
@@ -13,6 +13,7 @@ import { swiftExportChecker } from '../export-detection.js';
13
13
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
14
14
  import { swiftImportConfig } from '../import-resolvers/configs/swift.js';
15
15
  import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
16
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
16
17
  import { createFieldExtractor } from '../field-extractors/generic.js';
17
18
  import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
18
19
  import { createMethodExtractor } from '../method-extractors/generic.js';
@@ -222,6 +223,8 @@ export const swiftProvider = defineLanguage({
222
223
  }),
223
224
  variableExtractor: createVariableExtractor(swiftVariableConfig),
224
225
  classExtractor: createClassExtractor(swiftClassConfig),
226
+ // ── Swift doc comments (`///`, `/** */`) → description (issue #2270) ──
227
+ descriptionExtractor: createLeadingDocDescriptionExtractor(),
225
228
  orderSameNameTypeCandidates: orderSwiftSameNameTypeCandidates,
226
229
  builtInNames: BUILT_INS,
227
230
  // ── Scope-based resolution hooks (RFC #909 Ring 3, issue #937). See
@@ -9,6 +9,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
9
9
  import { defineLanguage } from '../language-provider.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { typescriptClassConfig, javascriptClassConfig, } from '../class-extractors/configs/typescript-javascript.js';
12
+ import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
12
13
  import { createTypeScriptCfgVisitor } from '../cfg/visitors/typescript.js';
13
14
  import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js';
14
15
  import { tsExportChecker } from '../export-detection.js';
@@ -287,6 +288,11 @@ export const typescriptProvider = defineLanguage({
287
288
  }),
288
289
  variableExtractor: createVariableExtractor(typescriptVariableConfig),
289
290
  classExtractor: createClassExtractor(typescriptClassConfig),
291
+ // ── JSDoc → description (issue #2270). An exported decl is captured as the
292
+ // inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
293
+ descriptionExtractor: createLeadingDocDescriptionExtractor({
294
+ wrapperNodeTypes: ['export_statement'],
295
+ }),
290
296
  builtInNames: BUILT_INS,
291
297
  // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
292
298
  // TypeScript is the third migration after Python and C#. See
@@ -347,6 +353,11 @@ export const javascriptProvider = defineLanguage({
347
353
  }),
348
354
  variableExtractor: createVariableExtractor(javascriptVariableConfig),
349
355
  classExtractor: createClassExtractor(javascriptClassConfig),
356
+ // ── JSDoc → description (issue #2270). An exported decl is captured as the
357
+ // inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
358
+ descriptionExtractor: createLeadingDocDescriptionExtractor({
359
+ wrapperNodeTypes: ['export_statement'],
360
+ }),
350
361
  builtInNames: BUILT_INS,
351
362
  // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
352
363
  // JavaScript is the fourth migration after Python, C#, and TypeScript.
@@ -31,6 +31,18 @@ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, unquot
31
31
  * @node → enclosing declaration (class_declaration | method_declaration)
32
32
  * @value → the string-literal argument
33
33
  * @key → the named-argument member key (absent for positional form)
34
+ *
35
+ * Method-level routes accept both the bare string form `@GetMapping("/x")` and
36
+ * the array form `@GetMapping({"/a","/b"})` (positional or `path =`/`value =`):
37
+ * a multi-element array yields one match per element, so the Phase 2 loop emits
38
+ * one route per path with no special-casing. This mirrors the group-layer
39
+ * `java.ts` query so the two Spring extractors stay in parity (#2138 follow-up;
40
+ * the divergence here was the root of the #2265 array-form gap). The class-level
41
+ * `@RequestMapping` branches also match the array form, but only to *detect* it:
42
+ * an array-form class prefix can't be resolved to a single string, so Phase 2
43
+ * suppresses that class's method-level array routes rather than emit them with a
44
+ * dropped prefix (a wrong route). Full class-array cross-product support is left
45
+ * to a follow-up (#2280).
34
46
  */
35
47
  const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
36
48
  [
@@ -38,7 +50,9 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
38
50
  (modifiers
39
51
  (annotation
40
52
  name: (identifier) @ann
41
- arguments: (annotation_argument_list (string_literal) @value)))) @node
53
+ arguments: (annotation_argument_list
54
+ [(string_literal) @value
55
+ (element_value_array_initializer (string_literal) @value)])))) @node
42
56
  (class_declaration
43
57
  (modifiers
44
58
  (annotation
@@ -46,12 +60,15 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
46
60
  arguments: (annotation_argument_list
47
61
  (element_value_pair
48
62
  key: (identifier) @key
49
- value: (string_literal) @value))))) @node
63
+ value: [(string_literal) @value
64
+ (element_value_array_initializer (string_literal) @value)]))))) @node
50
65
  (method_declaration
51
66
  (modifiers
52
67
  (annotation
53
68
  name: (identifier) @ann
54
- arguments: (annotation_argument_list (string_literal) @value)))) @node
69
+ arguments: (annotation_argument_list
70
+ [(string_literal) @value
71
+ (element_value_array_initializer (string_literal) @value)])))) @node
55
72
  (method_declaration
56
73
  (modifiers
57
74
  (annotation
@@ -59,7 +76,8 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
59
76
  arguments: (annotation_argument_list
60
77
  (element_value_pair
61
78
  key: (identifier) @key
62
- value: (string_literal) @value))))) @node
79
+ value: [(string_literal) @value
80
+ (element_value_array_initializer (string_literal) @value)]))))) @node
63
81
  ]
64
82
  `);
65
83
  /**
@@ -76,8 +94,15 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
76
94
  */
77
95
  export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
78
96
  const matches = ROUTE_ANNOTATION_QUERY.matches(tree.rootNode);
79
- // Phase 1: collect class-level @RequestMapping prefixes keyed by node id
97
+ // Phase 1: collect class-level @RequestMapping prefixes keyed by node id.
98
+ // A scalar prefix (`@RequestMapping("/base")`) is stored in prefixByClassId.
99
+ // A class whose @RequestMapping uses the array form (`@RequestMapping({...})`)
100
+ // is instead recorded in classesWithArrayPrefix: there is no single prefix to
101
+ // store, and Phase 2 uses this to suppress that class's method-level array
102
+ // routes rather than emit them unprefixed (a wrong route — see #2280). Full
103
+ // class-array cross-product support is out of scope here.
80
104
  const prefixByClassId = new Map();
105
+ const classesWithArrayPrefix = new Set();
81
106
  for (const match of matches) {
82
107
  const caps = {};
83
108
  for (const { name, node } of match.captures) {
@@ -92,6 +117,10 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
92
117
  if (node.type === 'class_declaration' && annNode.text === 'RequestMapping') {
93
118
  if (!isRouteMemberKey(keyNode))
94
119
  continue;
120
+ if (valueNode.parent?.type === 'element_value_array_initializer') {
121
+ classesWithArrayPrefix.add(node.id);
122
+ continue;
123
+ }
95
124
  const prefix = unquoteSpringLiteral(valueNode.text);
96
125
  if (prefix !== null)
97
126
  prefixByClassId.set(node.id, prefix);
@@ -122,6 +151,18 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
122
151
  if (routePath === null)
123
152
  continue;
124
153
  const enclosingClass = findEnclosingClass(node);
154
+ // Suppress a method-level *array-form* route nested under a class-level
155
+ // array-form @RequestMapping. The class prefix is one of several values that
156
+ // cannot be resolved to a single string here, so emitting the route would
157
+ // drop the prefix and yield a wrong unprefixed Route (a false signal, worse
158
+ // than a missing one). Skipping keeps ingestion a strict subset of the group
159
+ // scan — safe under routeCoverage:'partial'. Full class-array cross-product
160
+ // support is tracked in #2280. (Scalar method paths under an array class
161
+ // prefix are left unchanged: that pre-existing divergence is out of scope.)
162
+ const isArrayElement = valueNode.parent?.type === 'element_value_array_initializer';
163
+ if (isArrayElement && enclosingClass && classesWithArrayPrefix.has(enclosingClass.id)) {
164
+ continue;
165
+ }
125
166
  const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
126
167
  // `node` is the annotated `method_declaration`; its name field is the
127
168
  // handler method name (resolved to a symbol UID later by the routes phase).
@@ -34,7 +34,7 @@ export declare const isQualifiableScopeLabel: (nodeLabel: string) => boolean;
34
34
  */
35
35
  export declare const DEFINITION_CAPTURE_KEYS: readonly ["definition.function", "definition.class", "definition.interface", "definition.method", "definition.struct", "definition.enum", "definition.namespace", "definition.module", "definition.trait", "definition.impl", "definition.type", "definition.const", "definition.static", "definition.variable", "definition.typedef", "definition.macro", "definition.union", "definition.property", "definition.record", "definition.delegate", "definition.annotation", "definition.constructor", "definition.template"];
36
36
  /** Extract the definition node from a tree-sitter query capture map. */
37
- export declare const getDefinitionNodeFromCaptures: (captureMap: Record<string, SyntaxNode>) => SyntaxNode | null;
37
+ export declare const getDefinitionNodeFromCaptures: (captureMap: Record<string, SyntaxNode | undefined>) => SyntaxNode | null;
38
38
  type QueryMatchLike = {
39
39
  captures: Array<{
40
40
  name: string;
@@ -87,7 +87,7 @@ export declare function findAncestorBeforeBoundary(node: SyntaxNode, targetTypes
87
87
  * (e.g. C/C++ duplicate skipping, Kotlin Method promotion).
88
88
  * Returns null if the capture should be skipped (import, call, C/C++ duplicate, missing name).
89
89
  */
90
- export declare function getLabelFromCaptures(captureMap: Record<string, SyntaxNode>, provider: LanguageProvider): NodeLabel | null;
90
+ export declare function getLabelFromCaptures(captureMap: Record<string, SyntaxNode | undefined>, provider: LanguageProvider): NodeLabel | null;
91
91
  /** Enclosing class info: both the generated node ID and the bare class name. */
92
92
  export interface EnclosingClassInfo {
93
93
  classId: string;
@@ -215,6 +215,85 @@ export declare function findDescendant(root: SyntaxNode, type: string): SyntaxNo
215
215
  export declare function extractStringContent(node: SyntaxNode | null | undefined): string | null;
216
216
  /** Find the first direct named child of a tree-sitter node matching the given type. */
217
217
  export declare function findChild(node: SyntaxNode, type: string): SyntaxNode | null;
218
+ /**
219
+ * Extract the normalized text of a leading doc comment immediately preceding a
220
+ * definition node — covering both block doc comments (Javadoc / KDoc / JSDoc /
221
+ * PHPDoc / Doxygen, opened by `/**` or `/*!`) and runs of line doc comments
222
+ * (`///`, `//!`, or the caller-supplied prefixes such as Go's `//` or Ruby's
223
+ * `#`). Returns `undefined` when there is no preceding doc comment or it is
224
+ * empty.
225
+ *
226
+ * Grammar-agnostic by design: matches on the comment text prefix rather than a
227
+ * grammar node type, because the comment node is named differently across
228
+ * grammars (`block_comment`, `multiline_comment`, `comment`, `line_comment`).
229
+ * Annotations and modifiers live inside the definition node, so the doc comment
230
+ * remains the definition's `previousNamedSibling` even on annotated/decorated
231
+ * declarations.
232
+ *
233
+ * Block comments are taken as the immediately-preceding sibling (intervening
234
+ * package/import/code siblings already shield a file-level license block from
235
+ * the first declaration). Line doc comments enforce row-adjacency: the first
236
+ * comment must sit on the line directly above the definition, and each comment
237
+ * walked further up must sit directly above the previous one — so a run stops
238
+ * at a blank line. This matches godoc/RDoc/rustdoc convention and prevents an
239
+ * unrelated comment block (a license header, a Ruby shebang + magic comment)
240
+ * separated by a blank line from being absorbed. Adjacency is checked on
241
+ * `startPosition.row` (reliable) rather than `endPosition.row`, since some
242
+ * grammars fold the trailing newline into the comment node.
243
+ *
244
+ * Normalization mirrors Python docstring handling: strip the comment delimiters
245
+ * / per-line markers, then collapse whitespace to single spaces so tag content
246
+ * (`@param`, `@deprecated since 2.0, use computeBalanceV2`) survives.
247
+ *
248
+ * When the captured definition is an inner node and its own preceding sibling
249
+ * carries no doc, the search retries from a wrapping node whose type is listed in
250
+ * `opts.wrapperNodeTypes` (e.g. an `export_statement` wrapping an exported
251
+ * function/class — the JSDoc precedes the wrapper, not the inner declaration).
252
+ */
253
+ export interface LeadingDocCommentOptions {
254
+ /** Line-comment doc prefixes (defaults to {@link DEFAULT_LINE_DOC_PREFIXES};
255
+ * Go passes `['//']`, Ruby passes `['#']`). */
256
+ lineCommentPrefixes?: readonly string[];
257
+ /** Grammar node types that wrap a definition such that the doc comment is the
258
+ * wrapper's preceding sibling rather than the definition's. TS/JS pass
259
+ * `['export_statement']`. Empty by default → no wrapper retry. */
260
+ wrapperNodeTypes?: readonly string[];
261
+ /** Line-comment prefixes that are tool/build directives or magic comments
262
+ * rather than documentation (Go passes `['//go:', '// +build', …]`, Ruby
263
+ * passes `['# frozen_string_literal:', '#!', …]`). A matching line is skipped
264
+ * in the doc run rather than absorbed. Empty by default. */
265
+ lineDirectivePrefixes?: readonly string[];
266
+ /** Block-comment doc openers (defaults to `['/**', '/*!']`). Rust passes
267
+ * `['/**']` so its inner-doc `/*!` does not attach to the following item. */
268
+ blockDocPrefixes?: readonly string[];
269
+ }
270
+ export declare function extractLeadingDocComment(node: SyntaxNode, opts?: LeadingDocCommentOptions): string | undefined;
271
+ /** Node labels that can carry a leading doc comment — callables and type-like
272
+ * declarations. Field/property/variable/const doc is intentionally excluded
273
+ * (issue #2270 scopes this to method/type documentation). Language-neutral:
274
+ * a label a given grammar never emits simply never matches.
275
+ *
276
+ * Bounded to labels that are also in `embeddings/types.ts` `EMBEDDABLE_LABELS`:
277
+ * the description is only useful once it reaches the embedding metadata header,
278
+ * and the embedding pipeline only queries embeddable labels. Extracting docs
279
+ * for a non-embeddable label is a wasted write that never becomes searchable.
280
+ * A subset invariant in the unit tests guards against drift. Making currently-
281
+ * non-embeddable doc-bearing labels (Module, Delegate, Annotation, and C++
282
+ * `Template`) searchable is tracked as a follow-up — it needs an embedding-
283
+ * pipeline/schema change beyond this fix. */
284
+ export declare const DOC_BEARING_LABELS: ReadonlySet<NodeLabel>;
285
+ /**
286
+ * Build a `LanguageProvider.descriptionExtractor` that surfaces a definition's
287
+ * leading doc comment as its `description` (issue #2270). For labels in
288
+ * {@link DOC_BEARING_LABELS} (which is bounded to embeddable labels) the text
289
+ * then reaches the embedding metadata header and becomes semantically searchable.
290
+ *
291
+ * Language-neutral factory (names no language): guards on
292
+ * {@link DOC_BEARING_LABELS}; callers pass per-language doc-comment behavior via
293
+ * {@link LeadingDocCommentOptions} (line prefixes, export-style wrappers, …)
294
+ * which is threaded straight through to {@link extractLeadingDocComment}.
295
+ */
296
+ export declare const createLeadingDocDescriptionExtractor: (opts?: LeadingDocCommentOptions) => ((nodeLabel: NodeLabel, nodeName: string, captureMap: Record<string, SyntaxNode | undefined>) => string | undefined);
218
297
  /** Convert a tree-sitter node to a `Capture` with 1-based line numbers
219
298
  * (matching RFC §2.1). The tag includes the leading `@`. */
220
299
  export declare function nodeToCapture(name: string, node: SyntaxNode): Capture;
@@ -824,6 +824,148 @@ export function findChild(node, type) {
824
824
  }
825
825
  return null;
826
826
  }
827
+ /** Remove bidi-override and zero-width control characters. Doc text is
828
+ * attacker-influenced (any indexed repo) and is returned verbatim to MCP
829
+ * clients, so strip Trojan-Source-style hidden controls from the description
830
+ * before it leaves the extractor (#2286 review). Scoped to the doc-comment path
831
+ * only — global `sanitizeUTF8` is intentionally untouched. */
832
+ const stripBidiAndZeroWidth = (text) => Array.from(text)
833
+ .filter((ch) => {
834
+ const c = ch.codePointAt(0) ?? 0;
835
+ // Bidi overrides/isolates (U+202A–202E, U+2066–2069), zero-width
836
+ // space/joiners (U+200B–200D), and BOM/zero-width-no-break (U+FEFF).
837
+ return !((c >= 0x202a && c <= 0x202e) ||
838
+ (c >= 0x2066 && c <= 0x2069) ||
839
+ (c >= 0x200b && c <= 0x200d) ||
840
+ c === 0xfeff);
841
+ })
842
+ .join('');
843
+ /** Normalize a block doc comment body: strip the opening (double-star or
844
+ * bang) delimiter, the closing delimiter, and per-line gutter stars, then
845
+ * collapse whitespace so tag content stays as searchable words. */
846
+ const normalizeBlockDocComment = (text) => {
847
+ const inner = stripBidiAndZeroWidth(text
848
+ .replace(/^\/\*[*!]/, '')
849
+ // Close delimiter: tolerate the degenerate empty comment `/**/`, where the
850
+ // opening strip already consumed the shared `*`, leaving a lone `/`.
851
+ .replace(/\*?\/\s*$/, '')
852
+ .replace(/^[ \t]*\*[ \t]?/gm, ' ')
853
+ .replace(/\s+/g, ' ')
854
+ .trim());
855
+ return inner.length > 0 ? inner : undefined;
856
+ };
857
+ /** Default line-comment prefixes treated as documentation: the universal
858
+ * triple-slash / bang-slash doc markers (Rust, C#, Dart, Swift, Doxygen).
859
+ * Go (`//`) and Ruby (`#`) opt into their conventional markers explicitly. */
860
+ const DEFAULT_LINE_DOC_PREFIXES = ['///', '//!'];
861
+ /** Default block-comment doc openers: Javadoc/JSDoc-style `/**` and Doxygen
862
+ * `/*!`. Rust opts out of `/*!` (and `//!`) because those are *inner* docs that
863
+ * document the enclosing item, not the following one. */
864
+ const DEFAULT_BLOCK_DOC_PREFIXES = ['/**', '/*!'];
865
+ /** A file-top `/** … *\/` license/copyright/file-overview block has no
866
+ * package/import sibling to shield it, so it would otherwise be absorbed as the
867
+ * first declaration's description (PR #2286 review). These markers identify such
868
+ * headers; they are specific enough not to fire on an ordinary symbol doc that
869
+ * merely mentions the word "copyright". `@file`/`@fileoverview` are explicitly
870
+ * file-level JSDoc tags, so a block carrying them is not a symbol doc. */
871
+ const FILE_HEADER_MARKER = /SPDX-License-Identifier|@licen[sc]e\b|@fileoverview\b|@file\b|Licen[sc]ed under|copyright\s*(\(c\)|©|\d{4})/i;
872
+ export function extractLeadingDocComment(node, opts = {}) {
873
+ const lineCommentPrefixes = opts.lineCommentPrefixes ?? DEFAULT_LINE_DOC_PREFIXES;
874
+ const wrapperNodeTypes = opts.wrapperNodeTypes ?? [];
875
+ const lineDirectivePrefixes = opts.lineDirectivePrefixes ?? [];
876
+ const blockDocPrefixes = opts.blockDocPrefixes ?? DEFAULT_BLOCK_DOC_PREFIXES;
877
+ const fromNode = (anchor) => {
878
+ const prev = anchor.previousNamedSibling;
879
+ if (!prev)
880
+ return undefined;
881
+ // Block doc comment: /** ... */ or /*! ... */
882
+ if (blockDocPrefixes.some((p) => prev.text.startsWith(p))) {
883
+ // Skip a file-top license/copyright/overview header (no package/import
884
+ // sibling shields it from the first declaration). A strict row-adjacency
885
+ // check is unreliable here — some grammars fold the trailing newline into
886
+ // the comment node — so match header markers instead.
887
+ if (FILE_HEADER_MARKER.test(prev.text))
888
+ return undefined;
889
+ return normalizeBlockDocComment(prev.text);
890
+ }
891
+ // Run of row-adjacent preceding line doc comments (e.g. `///` or `//`).
892
+ const matchedPrefix = (text) => lineCommentPrefixes.find((prefix) => text.trimStart().startsWith(prefix));
893
+ const isDirective = (text) => lineDirectivePrefixes.some((prefix) => text.trimStart().startsWith(prefix));
894
+ const lines = [];
895
+ let current = prev;
896
+ let expectedRow = anchor.startPosition.row - 1;
897
+ while (current) {
898
+ const text = current.text;
899
+ const prefix = matchedPrefix(text);
900
+ if (prefix === undefined || current.startPosition.row !== expectedRow)
901
+ break;
902
+ // A build/tool directive or magic comment (e.g. `//go:build`,
903
+ // `# frozen_string_literal:`) is not documentation: skip it but keep
904
+ // walking the adjacent run, so a real doc above it is still collected.
905
+ if (!isDirective(text))
906
+ lines.unshift(text.trimStart().slice(prefix.length));
907
+ expectedRow = current.startPosition.row - 1;
908
+ current = current.previousNamedSibling;
909
+ }
910
+ const joined = stripBidiAndZeroWidth(lines.join(' ').replace(/\s+/g, ' ').trim());
911
+ return joined.length > 0 ? joined : undefined;
912
+ };
913
+ const direct = fromNode(node);
914
+ if (direct !== undefined)
915
+ return direct;
916
+ const parent = node.parent;
917
+ if (parent && wrapperNodeTypes.includes(parent.type)) {
918
+ return fromNode(parent);
919
+ }
920
+ return undefined;
921
+ }
922
+ /** Node labels that can carry a leading doc comment — callables and type-like
923
+ * declarations. Field/property/variable/const doc is intentionally excluded
924
+ * (issue #2270 scopes this to method/type documentation). Language-neutral:
925
+ * a label a given grammar never emits simply never matches.
926
+ *
927
+ * Bounded to labels that are also in `embeddings/types.ts` `EMBEDDABLE_LABELS`:
928
+ * the description is only useful once it reaches the embedding metadata header,
929
+ * and the embedding pipeline only queries embeddable labels. Extracting docs
930
+ * for a non-embeddable label is a wasted write that never becomes searchable.
931
+ * A subset invariant in the unit tests guards against drift. Making currently-
932
+ * non-embeddable doc-bearing labels (Module, Delegate, Annotation, and C++
933
+ * `Template`) searchable is tracked as a follow-up — it needs an embedding-
934
+ * pipeline/schema change beyond this fix. */
935
+ export const DOC_BEARING_LABELS = new Set([
936
+ 'Function',
937
+ 'Method',
938
+ 'Constructor',
939
+ 'Class',
940
+ 'Interface',
941
+ 'Enum',
942
+ 'Struct',
943
+ 'Trait',
944
+ 'Record',
945
+ 'Union',
946
+ 'Namespace',
947
+ 'TypeAlias',
948
+ 'Macro',
949
+ ]);
950
+ /**
951
+ * Build a `LanguageProvider.descriptionExtractor` that surfaces a definition's
952
+ * leading doc comment as its `description` (issue #2270). For labels in
953
+ * {@link DOC_BEARING_LABELS} (which is bounded to embeddable labels) the text
954
+ * then reaches the embedding metadata header and becomes semantically searchable.
955
+ *
956
+ * Language-neutral factory (names no language): guards on
957
+ * {@link DOC_BEARING_LABELS}; callers pass per-language doc-comment behavior via
958
+ * {@link LeadingDocCommentOptions} (line prefixes, export-style wrappers, …)
959
+ * which is threaded straight through to {@link extractLeadingDocComment}.
960
+ */
961
+ export const createLeadingDocDescriptionExtractor = (opts = {}) => {
962
+ return (nodeLabel, _nodeName, captureMap) => {
963
+ if (!DOC_BEARING_LABELS.has(nodeLabel))
964
+ return undefined;
965
+ const definitionNode = getDefinitionNodeFromCaptures(captureMap);
966
+ return definitionNode ? extractLeadingDocComment(definitionNode, opts) : undefined;
967
+ };
968
+ };
827
969
  // ============================================================================
828
970
  // Capture + range helpers (formerly python/ast-utils.ts — language-agnostic)
829
971
  // ============================================================================
@@ -1589,7 +1589,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1589
1589
  }
1590
1590
  }
1591
1591
  const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${parameterShapeTag}${constraintsTag}`);
1592
- const description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
1592
+ let description;
1593
+ try {
1594
+ description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
1595
+ }
1596
+ catch (err) {
1597
+ // A throw here (an unexpected tree-sitter node shape, a provider bug) must
1598
+ // NOT propagate — it would escape processFileGroup to the language-group
1599
+ // catch, which treats any throw as "parser unavailable" and silently drops
1600
+ // every remaining file in the group. Mirrors the extractTemplateConstraints
1601
+ // guard above (#2286 review).
1602
+ reportWarning(`Description extraction failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
1603
+ description = undefined;
1604
+ }
1593
1605
  let frameworkHint = definitionNode
1594
1606
  ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
1595
1607
  : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.9-rc.13",
3
+ "version": "1.6.9-rc.15",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",