mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat(routes): extract Spring method-level array-form routes in ingestion + extractor parity test (#2138 follow-up)
ingestion's `extractSpringRoutes` (route-extractors/spring.ts) matched only a
single string literal on `@(Get|...)Mapping`, so the array form
`@GetMapping({"/a","/b"})` produced no graph Route node — while the group-layer
`java.ts` scan did match it. That divergence was the root of the #2265 array-form
parse-skip gap.
- spring.ts: add the array-form alternation
`[(string_literal) @value (element_value_array_initializer (string_literal) @value)]`
to the two method-declaration query branches (positional + `path=`/`value=`),
mirroring the group query. A multi-element array yields one match per element,
so the Phase 2 loop emits one route per path with no other change. Class-level
`@RequestMapping` array prefixes remain single-literal (rare; left to a
follow-up).
- test: spring-route-parity runs one shared Java fixture through BOTH extractors
(ingestion `extractSpringRoutes` + group `JAVA_HTTP_PLUGIN.scan`) and asserts
identical provider {method,path} sets — the parity guard the maintainer asked
for in #2078, so the two Spring extractors can't silently drift again
(verified: reverting the array branch turns the parity test red).
This commit is contained in:
parent
d27fd11c4b
commit
1630df87ae
2 changed files with 111 additions and 2 deletions
|
|
@ -39,6 +39,15 @@ import {
|
|||
* @node → enclosing declaration (class_declaration | method_declaration)
|
||||
* @value → the string-literal argument
|
||||
* @key → the named-argument member key (absent for positional form)
|
||||
*
|
||||
* Method-level routes accept both the bare string form `@GetMapping("/x")` and
|
||||
* the array form `@GetMapping({"/a","/b"})` (positional or `path =`/`value =`):
|
||||
* a multi-element array yields one match per element, so the Phase 2 loop emits
|
||||
* one route per path with no special-casing. This mirrors the group-layer
|
||||
* `java.ts` query so the two Spring extractors stay in parity (#2138 follow-up;
|
||||
* the divergence here was the root of the #2265 array-form gap). Array form on a
|
||||
* class-level `@RequestMapping` prefix is not matched here yet (rare; left to a
|
||||
* follow-up) — `@value` on the class branches stays a single string literal.
|
||||
*/
|
||||
const ROUTE_ANNOTATION_QUERY = new Parser.Query(
|
||||
Java,
|
||||
|
|
@ -61,7 +70,9 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(
|
|||
(modifiers
|
||||
(annotation
|
||||
name: (identifier) @ann
|
||||
arguments: (annotation_argument_list (string_literal) @value)))) @node
|
||||
arguments: (annotation_argument_list
|
||||
[(string_literal) @value
|
||||
(element_value_array_initializer (string_literal) @value)])))) @node
|
||||
(method_declaration
|
||||
(modifiers
|
||||
(annotation
|
||||
|
|
@ -69,7 +80,8 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(
|
|||
arguments: (annotation_argument_list
|
||||
(element_value_pair
|
||||
key: (identifier) @key
|
||||
value: (string_literal) @value))))) @node
|
||||
value: [(string_literal) @value
|
||||
(element_value_array_initializer (string_literal) @value)]))))) @node
|
||||
]
|
||||
`,
|
||||
);
|
||||
|
|
|
|||
97
gitnexus/test/unit/group/spring-route-parity.test.ts
Normal file
97
gitnexus/test/unit/group/spring-route-parity.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Parity test for the two Spring route extractors (#2078 maintainer request,
|
||||
* #2138 follow-up).
|
||||
*
|
||||
* GitNexus parses Spring `@(Get|Post|...)Mapping` annotations in TWO places:
|
||||
* - ingestion `route-extractors/spring.ts` → `extractSpringRoutes` (produces
|
||||
* graph `Route` nodes)
|
||||
* - group `http-patterns/java.ts` → `JAVA_HTTP_PLUGIN.scan` (produces
|
||||
* cross-repo HTTP contracts)
|
||||
*
|
||||
* They serve different layers and stay separate, but they MUST agree on the
|
||||
* set of provider (method, path) routes they recognise for the same source —
|
||||
* otherwise the graph under-covers what the group scan sees, which is exactly
|
||||
* the divergence behind the #2265 array-form gap (the group query matched
|
||||
* `@GetMapping({"/a","/b"})`, ingestion's didn't). This test runs one shared
|
||||
* fixture through both and asserts the provider sets are identical, so the two
|
||||
* can't silently drift again.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Parser from 'tree-sitter';
|
||||
import Java from 'tree-sitter-java';
|
||||
import { extractSpringRoutes } from '../../../src/core/ingestion/route-extractors/spring.js';
|
||||
import { JAVA_HTTP_PLUGIN } from '../../../src/core/group/extractors/http-patterns/java.js';
|
||||
|
||||
function parse(src: string): Parser.Tree {
|
||||
const p = new Parser();
|
||||
p.setLanguage(Java);
|
||||
return p.parse(src);
|
||||
}
|
||||
|
||||
/** Canonical `METHOD /a/b` form so prefix-join / slash / case differences wash out. */
|
||||
function canon(method: string, ...segments: string[]): string {
|
||||
const path = `/${segments.join('/').split('/').filter(Boolean).join('/')}`;
|
||||
return `${method.toUpperCase()} ${path.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/** ingestion side: join the class prefix + method path the way the routes phase does. */
|
||||
function ingestionProviders(src: string): Set<string> {
|
||||
return new Set(
|
||||
extractSpringRoutes(parse(src), 'X.java').map((r) =>
|
||||
canon(r.httpMethod, r.prefix ?? '', r.routePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** group side: provider detections (path already prefix-joined by the plugin). */
|
||||
function groupProviders(src: string): Set<string> {
|
||||
return new Set(
|
||||
JAVA_HTTP_PLUGIN.scan(parse(src))
|
||||
.filter((d) => d.role === 'provider')
|
||||
.map((d) => canon(d.method, d.path)),
|
||||
);
|
||||
}
|
||||
|
||||
describe('Spring route extractor parity — ingestion spring.ts vs group java.ts', () => {
|
||||
it('agree on bare, named-arg, and array-form method routes under a class prefix', () => {
|
||||
const src = `package com.example;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
public class OrderController {
|
||||
@GetMapping("/list") public Object list() { return null; }
|
||||
@PostMapping(path = "/make") public Object make() { return null; }
|
||||
@PutMapping(value = "/update") public Object update() { return null; }
|
||||
@GetMapping({"/a", "/b"}) public Object multi() { return null; }
|
||||
}
|
||||
`;
|
||||
const ingestion = ingestionProviders(src);
|
||||
const group = groupProviders(src);
|
||||
|
||||
// The array form is the regression that motivated this: both must see all four.
|
||||
expect(group).toEqual(
|
||||
new Set([
|
||||
'GET /api/orders/list',
|
||||
'POST /api/orders/make',
|
||||
'PUT /api/orders/update',
|
||||
'GET /api/orders/a',
|
||||
'GET /api/orders/b',
|
||||
]),
|
||||
);
|
||||
expect(ingestion).toEqual(group);
|
||||
});
|
||||
|
||||
it('agree on a no-prefix controller with a positional array', () => {
|
||||
const src = `package com.example;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class PlainController {
|
||||
@GetMapping("/solo") public Object solo() { return null; }
|
||||
@DeleteMapping({"/x", "/y", "/z"}) public Object many() { return null; }
|
||||
}
|
||||
`;
|
||||
expect(ingestionProviders(src)).toEqual(groupProviders(src));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue