Skip to content

fix(group): handle named annotation args in Java Spring route extraction#1834

Merged
magyargergo merged 6 commits into
abhigyanpatwari:mainfrom
henry201605:fix/java-spring-named-annotation-args
May 27, 2026
Merged

fix(group): handle named annotation args in Java Spring route extraction#1834
magyargergo merged 6 commits into
abhigyanpatwari:mainfrom
henry201605:fix/java-spring-named-annotation-args

Conversation

@henry201605

Copy link
Copy Markdown
Contributor

Problem

The Java HTTP source-scan plugin fails to extract routes when Spring annotations
use named arguments:

@RequestMapping(path = "/translator")   // class prefix lost
@GetMapping(value = "/download/{id}")   // method route missed entirely

Only the positional form @RequestMapping("/translator") was handled.

This causes cross-repo contract matching to fail — providers are extracted
without their class-level prefix (e.g. /findtranslatedlistpage instead of
/translator/findtranslatedlistpage), so group sync reports them as unmatched.

Root Cause

The tree-sitter AST differs between positional and named annotation arguments:

  • @RequestMapping("/api")annotation_argument_list > string_literal
  • @RequestMapping(path = "/api")annotation_argument_list > element_value_pair > string_literal

The existing queries only matched the first shape.

Fix

Add a second pattern to both SPRING_CLASS_PREFIX_PATTERNS and
SPRING_METHOD_ROUTE_PATTERNS that matches element_value_pair > value: (string_literal).

Testing

  • All 33 existing http-route-extractor.test.ts tests pass
  • npx tsc --noEmit clean
  • Manual verification with real Spring controllers using all three annotation styles

The Java HTTP plugin only matched positional `@RequestMapping("/path")`
syntax for class-level prefixes and method-level routes. Named argument
forms (`path = "/path"` and `value = "/path"`) produce an
`element_value_pair` AST node that the tree-sitter queries did not cover,
causing the class prefix to be lost and named-arg method routes to be
missed entirely during cross-repo contract extraction.

Add a second pattern to both SPRING_CLASS_PREFIX_PATTERNS and
SPRING_METHOD_ROUTE_PATTERNS matching the element_value_pair structure.
@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
9988 9984 0 4 590s

✅ All 9984 tests passed

4 test(s) skipped — expand for details
  • PHP pipeline benchmark > scales with file count (workers enabled)
  • Ruby pipeline benchmark > scales with file count (workers enabled)
  • Rust pipeline benchmark > scales with file count (workers enabled)
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 79.72% 34733/43565 79.48% 📈 +0.2 🟢 ███████████████░░░░░
Branches 68.25% 22150/32450 68% 📈 +0.2 🟢 █████████████░░░░░░░
Functions 84.65% 3598/4250 84.56% 📈 +0.1 🟢 ████████████████░░░░
Lines 83.21% 31282/37594 82.97% 📈 +0.2 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 5m 5s —— View job


PR #1834 Review — fix(group): handle named annotation args in Java Spring route extraction

Tasks

  • Post initial comment
  • Read required repo docs (DoD.md, GUARDRAILS.md — CONTRIBUTING.md, TESTING.md, ARCHITECTURE.md skimmed for context)
  • Inspect PR diff and branch hygiene
  • Read changed file gitnexus/src/core/group/extractors/http-patterns/java.ts
  • Read related files (extractor, types, tree-sitter-scanner, tests, topic-patterns/java.ts)
  • Analyze tree-sitter query correctness — Issue A confirmed
  • Analyze Issue B: path= vs value= handling
  • Analyze Issue C: missing regression tests — confirmed
  • Analyze Issue D: array-valued Spring mappings
  • Analyze Issue E: empty method path behavior
  • Analyze Issue F: interface/provider attribution interaction
  • Unicode/bidi hygiene check

Review bar for this PR

Adversarial production-readiness review. The query touches Spring contract extraction directly — a tiny overbroad capture poisons contract IDs across all Java Spring services analyzed by GitNexus. The bar is correctness-first, not CI-green.


Problem being solved

Spring annotations accept named arguments in addition to the positional (shorthand) form. The existing positional patterns (annotation_argument_list (string_literal)) do not match the named forms path = "/api" or value = "/api". The PR adds a second pattern for each bundle to handle element_value_pair nodes.


Current PR state

Open. Single functional commit (8b6fa6e). Only gitnexus/src/core/group/extractors/http-patterns/java.ts changed (+31/-0). No test files changed. CI reports green (9869/9869 tests pass). The CI passing is not sufficient: as shown below, the existing tests do not exercise the new named-arg code path at all.


Merge status and mergeability

The branch has been synced with main (merge commit c4fce0c). No conflict markers found. CI is green. However: mergeability is blocked on correctness, not on CI.


Repository history considered


Branch hygiene assessment

Clean. One functional commit, one file, no unrelated churn, no reformatting of surrounding code.


Understanding of the change

The PR adds a second PatternSpec to both SPRING_CLASS_PREFIX_PATTERNS and SPRING_METHOD_ROUTE_PATTERNS that matches the tree-sitter element_value_pair node (used for named annotation arguments like path = "..." and value = "..."). The first pattern in each bundle handles the positional form (existing, untouched). The new patterns target:

(element_value_pair
  value: (string_literal) @prefix)   // class bundle
  
(element_value_pair
  value: (string_literal) @path)     // method bundle

Findings


Finding A — Query captures ALL named annotation attributes with string values, not just path/value ✴ BLOCKS MERGE

Risk: Any element_value_pair whose value is a string_literal is captured, regardless of the annotation member name. This means @GetMapping(produces = "application/json"), @GetMapping(name = "listUsers"), @RequestMapping(headers = "X-Foo=bar"), and @RequestMapping(consumes = "application/json", value = "/users") all emit spurious route matches.

Evidence (conclusive):

In tree-sitter's Java grammar, element_value_pair has two named fields: key: (identifier) (the annotation member name) and value: <element_value> (the right-hand side). The query value: (string_literal) is a tree-sitter field selector, not a filter on annotation member names. It matches the value field of any element_value_pair — whether the key is path, value, name, produces, consumes, headers, or anything else.

The existing codebase in the same directory already demonstrates the correct pattern. gitnexus/src/core/group/extractors/topic-patterns/java.ts:31–33:

(element_value_pair
  key: (identifier) @key (#eq? @key "topics")
  value: (string_literal) @value)

The PR's new patterns omit the key: constraint entirely.

Concrete false-positive scenarios:

Annotation Matched capture Emitted route (bogus)
@GetMapping(produces = "application/json") "application/json" http::GET::/application/json
@GetMapping(name = "listUsers", value = "/users") "listUsers" AND "/users" http::GET::/listUsers (extra) + http::GET::/users (correct)
@RequestMapping(headers = "X-Foo=bar", path = "/api") "X-Foo=bar" AND "/api" Both are captured; order-dependent — if headers is listed after path, "X-Foo=bar" is the final value in prefixByClassId for this class
@PostMapping(consumes = "application/json", value = "/orders") both http::POST::/application/json (extra)

For the class prefix case, prefixByClassId.set(classNode.id, prefix) is called for every match. Since runCompiledPatterns iterates all matches in document order (tree-sitter match ordering), the last element_value_pair in the annotation sets the class prefix. So @RequestMapping(path = "/api", name = "myApi") would use "myApi" as the class prefix — stripping the correct prefix entirely and producing routes like http::GET::/myApi/users.

For the method route case, each element_value_pair match produces a separate HttpDetection via out.push(...), so you get one detection per named attribute with a string value — multiple bogus contracts per method.

Recommended fix: Add key: (identifier) @key (#match? @key "^(path|value)$") to both new element_value_pair patterns, exactly as topic-patterns/java.ts already does for topics and queues:

(element_value_pair
  key: (identifier) @key (#match? @key "^(path|value)$")
  value: (string_literal) @prefix)

Fix this →

Blocks merge: YES — confirmed false positive route emission.


Finding B — value: field in tree-sitter query covers BOTH path = and value = forms ✅ Not a bug

Risk: The PR description states named arguments include both path = "/api" and value = "/api". There was a concern that value: (string_literal) in the query might only match the annotation member named value.

Evidence: value: in the query is a tree-sitter field name selector, not a string match. It selects the value field of element_value_pair, which is the right-hand side of any named argument — regardless of the key. So path = "/api" produces element_value_pair { key: "path", value: string_literal("/api") } and value = "/api" produces element_value_pair { key: "value", value: string_literal("/api") }. Both are captured by value: (string_literal).

Conclusion: Both path = and value = forms are handled. Finding B is not a bug — but it is precisely why Finding A is a confirmed problem: the over-matching is what enables both to work.

Blocks merge: NO


Finding C — No regression tests added ✴ BLOCKS MERGE

Risk: The PR changes only production TypeScript. Zero test files are modified. The existing http-route-extractor.test.ts tests use exclusively positional annotation forms (@RequestMapping("/api/v2"), @GetMapping("/users")). The bug being fixed — named annotation arguments — is not exercised by any existing test.

Evidence: git diff --name-only for the PR shows only gitnexus/src/core/group/extractors/http-patterns/java.ts. The existing test suite passes because it doesn't call the new code path at all. The new patterns can be deleted or broken without any test failing.

From DoD.md §2.7: "Tests cover the real changed path — they would fail if behavior, wiring, or contracts were broken, not only if a mock were misconfigured."

From DoD.md §6 "Not Done" signals: "The runtime path is not actually exercised by the tests."

Missing tests (minimum required):

  • Class-level @RequestMapping(path = "/api") → class prefix extracted correctly
  • Class-level @RequestMapping(value = "/api") → class prefix extracted correctly
  • Method-level @GetMapping(value = "/users") → route extracted correctly
  • Method-level @GetMapping(path = "/users") → route extracted correctly
  • Mixed class named + method positional: @RequestMapping(path = "/api") + @GetMapping("/users") → combined path correct
  • Mixed class positional + method named: @RequestMapping("/api") + @GetMapping(value = "/users") → combined path correct
  • Non-route attributes should NOT produce routes: @GetMapping(produces = "application/json") → no provider detection
  • Multi-attribute: @GetMapping(name = "listUsers", value = "/users") → exactly one provider, path "/users"
  • Class multi-attribute with non-route key: @RequestMapping(name = "myApi", path = "/api") → prefix is "/api", not "myApi"

Recommended fix: Add focused tests to gitnexus/test/unit/group/http-route-extractor.test.ts in the 'provider extraction — source-scan fallback (Strategy B)' describe block.

Blocks merge: YES — per DoD §2.7 and §6.


Finding D — Array-valued Spring annotations still not handled ℹ️ Not blocking, pre-existing limitation

Risk: Spring path and value are String[]. @GetMapping({"/a", "/b"}) and @GetMapping(value = {"/a", "/b"}) are not handled.

Evidence: Both the existing positional patterns and the new named patterns only match string_literal, not array_initializer. This is a pre-existing gap — the PR does not regress it. The PR description does not claim array support.

Recommended fix: Document as a known limitation or track against #1225. Not required for this PR.

Blocks merge: NO


Finding E — Empty method path with named arg is consistent with positional behavior ✅ Not a bug

Risk: @GetMapping(value = "") on a method with a class prefix might produce a trailing-slash contract ID.

Evidence: joinPath(prefix, "") returns "/" + cleanPrefix + "/" — a trailing slash. This is identical to the positional @GetMapping("") behavior (pre-existing, not introduced here). No regression.

Blocks merge: NO


Finding F — Interface attribution unchanged ✅ Not a problem

Risk: Adding named-arg support to class-level @RequestMapping might expand false provider attribution for annotated interfaces.

Evidence: The new patterns use (class_declaration ...) — not (interface_declaration ...). Interface-annotated types are unchanged by this PR. No interaction with draft PR #1743.

Blocks merge: NO


Hidden Unicode / bidi check ✅ Clean

Non-ASCII characters in the changed file are decorative box-drawing characters (, ) used in section-header comments, matching the pre-existing file style. No hidden bidi controls detected.


PR-specific assessment sections

1. Spring annotation semantics lane: Both path and value scalar string cases are handled (when the key filter is fixed). Array-valued forms (String[]) are not handled, but were never handled and the PR does not claim they are. This is acceptable scope.

2. Tree-sitter query correctness lane: FAILED. The value: field selector is a tree-sitter structural field name, not a filter on annotation member names. The query matches produces, consumes, headers, name, params, etc. — any named attribute with a string value. The sibling file topic-patterns/java.ts demonstrates the required key: constraint. The PR author appears to have confused the tree-sitter field selector syntax with annotation member name matching.

3. Group sync / contract matching lane: Bogus contracts like http::GET::/application/json and http::GET::/listUsers would be emitted and synced as provider contracts. These would never match real consumers, producing false unmatch reports — the opposite of the stated fix. In multi-attribute annotations where a non-route key precedes the route key (e.g., @RequestMapping(headers = "X-Foo=bar", path = "/api")), the class prefix could be set to "X-Foo=bar" instead of "/api", corrupting every route under that controller.

4. Regression compatibility lane: Existing positional patterns are untouched. Consumer extraction (RestTemplate, WebClient, OkHttp) is untouched. Graph-assisted strategy still takes precedence. No regression in existing behavior.

5. Tests and CI lane: CI is green because the existing tests don't exercise the new code path at all. The new code path has zero test coverage. This fails the DoD §2.7 bar explicitly.

6. Repository-history alignment lane: The PR is correctly narrow — it does not claim to solve constants, arrays, property placeholders, interfaces, or graph ingestion. It is consistent with the group source-scan plugin architecture introduced after PR #619 was closed.


Back-and-forth avoided by verifying

  • Is value: the tree-sitter field or the annotation member name? → Verified via topic-patterns/java.ts in the same repo, which proves the field is value: (structural) and the key name is filtered separately via key: (identifier) @key (#eq? @key "...").
  • Does the existing test suite cover named args? → Verified by reading all test cases: only positional forms used in all Spring Java fixtures.
  • Does the query actually handle path = forms? → Yes, because value: captures the right-hand side of any pair, including path = "/api" — but that same fact makes it capture produces = "application/json" too.
  • Are merges/conflicts an issue? → Branch is cleanly merged with main; no conflict.

Open questions

None required. All key issues have been verified directly from code.


Final verdict

not production-ready

The new element_value_pair queries lack a key: field constraint, which exists in the adjacent topic-patterns/java.ts and is required in tree-sitter to restrict matching to annotation members named path or value. Without it, the query captures any named attribute with a string value — name, produces, consumes, headers, params — emitting bogus route contracts that will appear as unmatched providers in group sync, which is the opposite of the stated fix. The required change is one line per pattern (key: (identifier) @key (#match? @key "^(path|value)$")). Additionally, the PR ships with zero regression tests for the new code path; the existing tests pass only because they never exercise the named-arg queries. Both issues must be resolved before merge per DoD §2.7 and §6.

@magyargergo

Copy link
Copy Markdown
Collaborator

@henry201605 could you please look into claude's findings? 🙏

henry added 2 commits May 27, 2026 09:54
… regression tests

Address Claude review on PR abhigyanpatwari#1834. The named-argument patterns added
in 8b6fa6e used `value: (string_literal)` (a tree-sitter field
selector for the right-hand side of element_value_pair), which matched
ANY annotation member with a string value — not just `path`/`value`.

Concrete fallout (without this fix):
  @GetMapping(produces = "application/json") → bogus http::GET::/application/json
  @GetMapping(name = "listUsers", value = "/users") → extra http::GET::/listUsers
  @RequestMapping(headers = "X-Foo=bar", path = "/api") → class prefix
    could be set to "X-Foo=bar" because prefixByClassId.set runs per
    match in document order, so the LAST element_value_pair wins.

The sibling topic-patterns/java.ts already demonstrates the correct
shape: constrain the `key:` field to the route member names.

This commit:
  - Adds `key: (identifier) @key (#match? @key "^(path|value)$")` to
    both SPRING_CLASS_PREFIX_PATTERNS and SPRING_METHOD_ROUTE_PATTERNS
    named-arg queries.
  - Adds 9 regression tests under
    `provider extraction — source-scan fallback (Strategy B)`:
      * @RequestMapping(path = "/api/v3") class prefix
      * @RequestMapping(value = "/orders") class prefix
      * @GetMapping(value = "/users") method route
      * @PostMapping(path = "/users") method route
      * mixed: class named-arg + method positional
      * mixed: class positional + method named-arg
      * @GetMapping(produces = "application/json") → no provider emitted
      * @GetMapping(name = "listUsers", value = "/users") → exactly one
        provider with path "/users", no /listUsers route
      * @RequestMapping(path = "/api", name = "myApi") → prefix is /api,
        not myApi (verifies the class-prefix overwrite scenario)

Tests: 42/42 pass in http-route-extractor.test.ts;
       522/522 pass under test/unit/group;
       npx tsc --noEmit clean.
…st verbatim

Claude review on PR abhigyanpatwari#1834 explicitly asked for the method-level
`@GetMapping(path = "/users")` case. The previous commit covered it
indirectly by exercising path= on @PostMapping (the Spring method
annotations share the same query, so any verb proves the path= field
is matched). Add a dedicated GET+path= test so the reviewer's
checklist is satisfied 1:1, and keep the POST+path= case as a bonus
verb-coverage test.

Tests: 43/43 pass in http-route-extractor.test.ts.
@henry201605

Copy link
Copy Markdown
Contributor Author

@magyargergo thanks for the review — addressed in 4ad7ec0a and 4c7c8d96 (just pushed):

Finding Akey: field constraint added to both new element_value_pair patterns, mirroring topic-patterns/java.ts:

(element_value_pair
  key: (identifier) @key (#match? @key "^(path|value)$")
  value: (string_literal) @prefix)

This stops the query from capturing non-route attributes like produces, consumes, headers, name, params.

Finding C — Added 10 regression tests under provider extraction — source-scan fallback (Strategy B):

# Case
1 Class @RequestMapping(path = "/api/v3") → prefix correct
2 Class @RequestMapping(value = "/orders") → prefix correct
3 Method @GetMapping(value = "/users")
4 Method @GetMapping(path = "/users")
5 Method @PostMapping(path = "/users") (verb-coverage bonus)
6 Mixed: class named-arg + method positional
7 Mixed: class positional + method named-arg
8 Anti-regression: @GetMapping(produces = "application/json") → no provider emitted
9 Anti-regression: @GetMapping(name = "...", value = "/users") → exactly one provider, no /listUsers
10 Anti-regression: @RequestMapping(path = "/api", name = "myApi") → prefix is /api, not myApi

Without the key: constraint, all three anti-regression tests fail (verified locally by reverting just the constraint and re-running).

Local validation:

  • http-route-extractor.test.ts — 43/43 ✅
  • test/unit/group — 522/522 ✅
  • npx tsc --noEmit — clean ✅

Findings B / D / E / F were either non-bugs or pre-existing limitations (e.g. String[] array values per #1225) that this PR explicitly does not claim to fix.

@magyargergo

Copy link
Copy Markdown
Collaborator

@henry201605 would it be difficult to add it to kotlin as well?

@magyargergo magyargergo merged commit eeea464 into abhigyanpatwari:main May 27, 2026
39 of 42 checks passed
@magyargergo

Copy link
Copy Markdown
Collaborator

@henry201605 Could you please raise a new PR for kotlin? 🙏

@henry201605

Copy link
Copy Markdown
Contributor Author

Hi @magyargergo, thanks for the nudge! Already raised as #1849 earlier today (ec505813).

A quick summary so the review is easy:

  • Same shape as the Java fix from fix(group): handle named annotation args in Java Spring route extraction #1834 — dual-pattern (positional + named), with the key: constraint pinned to ^(path|value)$ on the named pattern. Mirrors topic-patterns/java.ts and the post-review state of the Java side, so the same false-positive class (produces=, name=, headers=, etc.) cannot leak through.

  • Tests — 11 new cases in http-route-extractor.test.ts under provider extraction — source-scan fallback (Strategy B), gated on the optional tree-sitter-kotlin grammar. Includes the 3 anti-regression cases that Claude flagged on the Java PR:

    • @GetMapping(produces = "application/json") → no provider emitted
    • @GetMapping(name = "x", value = "/users") → exactly one provider, path /users
    • @RequestMapping(path = "/api", name = "myApi") → prefix stays /api regardless of attribute order

    I also reverse-validated by removing just the #match? constraint locally — precisely those 3 cases fail, which I think is the cleanest signal that the constraint is doing real work.

Scope note (worth flagging up front): this PR is providers only. Kotlin-side consumer detection (RestTemplate / WebClient / OkHttp call sites) is intentionally deferred to a follow-up. The Kotlin call-site AST (infix calls, extension functions, apply/let/with scope functions, trailing lambdas) is shaped quite differently from Java's method_invocation, so a careful, focused PR felt safer than bundling both in here. Existing Java consumer detection is unaffected, and Kotlin services that only define Spring routes (the common case for the issue you raised) are fully covered.

Happy to iterate on anything that comes up in review — and I'll open the consumer-side follow-up PR right after this one lands if that ordering works for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants