Skip to content

feat(group): extract OpenFeign @RequestLine consumer contracts#1904

Merged
magyargergo merged 9 commits into
abhigyanpatwari:mainfrom
henry201605:feat/java-feign-request-line
May 30, 2026
Merged

feat(group): extract OpenFeign @RequestLine consumer contracts#1904
magyargergo merged 9 commits into
abhigyanpatwari:mainfrom
henry201605:feat/java-feign-request-line

Conversation

@henry201605

Copy link
Copy Markdown
Contributor

Adds Java HTTP plugin support for the native OpenFeign annotation @RequestLine("METHOD /path"). Previously only @FeignClient interfaces using Spring MVC method annotations (@GetMapping etc.) were detected; the native annotation form — required by Feign Builder users and non-Spring Feign deployments — was silently ignored.

Why

OpenFeign’s @RequestLine is the canonical annotation for the framework (https://github.com/OpenFeign/feign#interface-annotations) and is the only option for clients built without Spring Cloud OpenFeign. Real-world Java microservices that mix Spring Cloud and plain Feign Builder clients (e.g. internal client-jar libraries shared across non-Spring services) fall back to manifest-link workarounds today because GitNexus produces no consumer contract for these methods.

What changed

  • FEIGN_REQUEST_LINE_PATTERNS — tree-sitter query covering both positional and named-arg (value =) forms.
  • parseRequestLine() — splits the verb + path string and drops any query portion, matching how RestTemplate/WebClient inline URL consumers are normalized.
  • The enclosing interface MUST carry @FeignClient; otherwise the detection is dropped to avoid false positives from same-named annotations in unrelated libraries.
  • Reuses the existing feignPrefixByInterfaceId map so @FeignClient(path=) and @RequestMapping interface prefixes apply uniformly across both Spring MVC and @RequestLine methods.
  • Confidence 0.75 — slightly higher than the 0.7 used for Spring MVC annotations because the verb is a string-literal value (less ambiguous than annotation-name inference).

Tests

Six new unit tests under extracts OpenFeign clients block:

Scenario Asserts
basic two methods both POST /ai/summarize and GET /ai/health extracted with framework: openfeign, confidence: 0.75
@FeignClient(path = "/api") prefix joining /api/orders/{param} for both GET and DELETE
query string stripping GET /search?q={query}&limit={limit}http::GET::/search
no @FeignClient annotation detection dropped (no false positive on plain interfaces)
mixed @RequestLine + @GetMapping on the same interface both produced at their respective confidences (0.75 / 0.7)
named-argument form @RequestLine(value = "...") extracted identically

Verification

  • npx tsc --noEmit
  • test/unit/group/http-route-extractor.test.ts — 83/83 (77 existing + 6 new) ✅
  • full test/unit/group/ — 31 files / 563 tests ✅
  • npx prettier --check on touched files ✅
  • npx eslint on touched files — 0 errors, 0 warnings introduced ✅

Adds Java HTTP plugin support for the native OpenFeign annotation
`@RequestLine("METHOD /path")`. Previously only `@FeignClient` interfaces
using Spring MVC method annotations (`@GetMapping` etc.) were detected;
the native annotation form — required by Feign Builder users and
non-Spring Feign deployments — was silently ignored.

Implementation:

- New `FEIGN_REQUEST_LINE_PATTERNS` covers both positional and named-arg
  (`value =`) forms.
- New `parseRequestLine()` parses the verb+path string and drops any
  query string (consistent with how RestTemplate/WebClient consumers
  handle inline literal URLs).
- The enclosing interface MUST carry `@FeignClient`; otherwise the
  detection is dropped to avoid false positives from same-named
  annotations in unrelated libraries.
- Reuses the existing `feignPrefixByInterfaceId` map so
  `@FeignClient(path=)` and `@RequestMapping` interface prefixes apply
  uniformly across both Spring MVC and `@RequestLine` methods.
- Confidence 0.75 — slightly higher than the 0.7 used for Spring MVC
  annotations because the verb is a string-literal value, not inferred
  from the annotation name (less ambiguous).

Six new unit tests cover: basic two-method extraction; `@FeignClient(path=)`
  prefix joining; query-string stripping; rejection of `@RequestLine` on
  non-Feign interfaces; mixing with `@GetMapping` on the same interface;
  named-argument form (`value = "..."`).

Verification: `npx tsc --noEmit`, full `test/unit/group` (31 files / 563
tests), `http-route-extractor.test.ts` (83/83 incl. 6 new), `prettier
--check` and `eslint` on touched files all pass.
@vercel

vercel Bot commented May 29, 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.

// @line → the literal `@RequestLine`
// @value → the request-line string literal (e.g. `"GET /users/{id}"`)
// @method → the enclosing method node, used for enclosing-interface lookup
const FEIGN_REQUEST_LINE_PATTERNS = compilePatterns({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, we could optimise all of the annotations into one compilePatterns. It seems to me that we do this a couple of times that we don't need to. Could you please merge all of the annotation match queries into one? 🙏

@henry201605 henry201605 May 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f10854d3 — both the positional and named-argument (value =) forms now share a single tree-sitter query via [(...) (...)] alternation, so compilePatterns builds one query and runCompiledPatterns issues one matches() pass.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give a more generic name to this please? 🙏

@github-actions

github-actions Bot commented May 29, 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
10189 10182 0 7 606s

✅ All 10182 tests passed

7 test(s) skipped — expand for details
  • COBOL pipeline benchmark > scales with file count
  • C# pipeline benchmark > scales with file count — namespaces spread across the solution
  • C# pipeline benchmark > scales with file count — all types in one (global) namespace bucket
  • 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.98% 35812/44776 79.48% 📈 +0.5 🟢 ███████████████░░░░░
Branches 68.64% 22892/33349 68% 📈 +0.6 🟢 █████████████░░░░░░░
Functions 84.95% 3681/4333 84.56% 📈 +0.4 🟢 ████████████████░░░░
Lines 83.52% 32248/38610 82.97% 📈 +0.5 🟢 ████████████████░░░░

📋 View full run · Generated by CI

…e query

Per @magyargergo's review on PR abhigyanpatwari#1904 — uses tree-sitter alternation
`[(...) (...)]` so the positional and named-argument forms of the
`@RequestLine` annotation are matched by a single compiled query and
invoked through one `runCompiledPatterns` pass instead of two.
@magyargergo

Copy link
Copy Markdown
Collaborator

Sorry I really meant it for all kinds of annotations that ends up as a request mapper

@henry201605

henry201605 commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@magyargergo Thanks for clarifying! I gave the full alternation merge a try and ran into a snag worth flagging.

The Spring @(Get|Post|Put|Delete|Patch)Mapping and @RequestMapping(path|value=...) queries rely on #match? predicates for the verb / argument-key regexes. Under our pinned tree-sitter@0.21 Node binding, #match? predicates inside [(...) (...)] alternation are silently dropped — the query compiles fine but matches() returns zero results. I confirmed it with a minimal repro:

// identical input + grammar; only the predicate differs
#eq?    alternation  1 match  
#match? alternation  0 matches 

That's why the @RequestLine merge in f10854d3 worked (it only uses #eq?), and why merging the Spring/Feign-prefix queries breaks 14 tests — the verb-name guard never fires.

Two ways to land your request:

  • (a) Pure alternation: expand every #match? into multiple #eq? branches (5 HTTP verbs × 2 argument shapes = up to ~15 branches per query). Behavior is correct, but the query strings grow significantly.
  • (b) Single compilePatterns(...) call per area, keeping multiple patterns: [...] specs inside it (so each predicate stays in its own bounded query). Same total matches() invocations as today, much less duplicated boilerplate, no binding workaround needed.

I'd lean toward (b) — it gets the consolidation win without the binding caveat. Happy to take the (a) route if you'd prefer the pure-alternation form, or do (b) and revisit (a) once we bump tree-sitter. Let me know which way you'd like to go 🙏

Comment thread gitnexus/src/core/group/extractors/http-patterns/java.ts Outdated
// @line → the literal `@RequestLine`
// @value → the request-line string literal (e.g. `"GET /users/{id}"`)
// @method → the enclosing method node, used for enclosing-interface lookup
const FEIGN_REQUEST_LINE_PATTERNS = compilePatterns({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please give a more generic name to this please? 🙏

Comment thread gitnexus/src/core/group/extractors/http-patterns/java.ts Outdated
Comment thread gitnexus/src/core/group/extractors/http-patterns/java.ts Outdated
henry and others added 3 commits May 29, 2026 22:36
…ant names

Per review feedback on abhigyanpatwari#1904 — renames the four route-mapper pattern
constants to framework-agnostic names (the per-constant comments already
document which framework each targets):
  SPRING_TYPE_PREFIX_PATTERNS     -> TYPE_PREFIX_PATTERNS
  FEIGN_REQUEST_LINE_PATTERNS     -> REQUEST_LINE_PATTERNS
  FEIGN_INTERFACE_PREFIX_PATTERNS -> INTERFACE_PREFIX_PATTERNS
  SPRING_METHOD_ROUTE_PATTERNS    -> METHOD_ROUTE_PATTERNS
@magyargergo

Copy link
Copy Markdown
Collaborator

@henry201605 To summarise what I'm looking for on the consolidation feedback:

Goal: one compilePatterns call (and therefore one matches() pass per file) for all request-mapper annotations — Spring @GetMapping / @PostMapping / @RequestMapping, OpenFeign @RequestLine, and the related prefix patterns — instead of separate pattern blocks each doing their own pass.

How it should work:

  1. A single tree-sitter query with alternation covers every annotation variant we care about.
  2. Use captures in the query to tag which variant matched (e.g. verb annotation name, @RequestLine value, path argument, enclosing method/interface).
  3. In the existing for-loop over matches, branch on the captured groups to build the consumer contract — no second/third runCompiledPatterns invocation.

The performance win is fewer tree-sitter passes; the behaviour should stay the same. Happy to discuss if anything in the current query shape makes a particular branch hard to capture cleanly.

@magyargergo

Copy link
Copy Markdown
Collaborator

@henry201605 Don't worry about this! I'll get it out today with the changes I want to see.

@magyargergo magyargergo self-assigned this May 30, 2026
@henry201605

Copy link
Copy Markdown
Contributor Author

@magyargergo Actually, I'd already taken a stab at the consolidation before seeing this — happy to push it for you to review, or just share the approach so you can take it from here, whatever's least disruptive.

One thing worth flagging that I hit: collapsing the route-mapper annotations into a single alternation with #match?/#any-of? predicates doesn't work on the pinned tree-sitter@0.21.1 binding — a predicate on any branch silently corrupts matching for the whole query (sibling #eq? branches drop their matches too). Minimal repro, same input: [positional(#eq?) | named(#match?)] → 1 match, [positional(#eq?) | named(#eq?)] → 2 matches.

How I worked around it: make the single query purely structural (class / interface / method × positional / named, zero predicates) and do all the annotation-name / key filtering in JS while iterating the matches — which lines up with your steps 2 & 3 (capture-tag + branch in the loop), just with the filtering in JS instead of query predicates. That collapses the four annotation pattern blocks (@RequestMapping prefixes, @(Get|Post|…)Mapping routes, Feign interface prefixes, @RequestLine) into one compilePatterns and one matches() pass. 84 tests green.

Scope note: the method-invocation consumers (RestTemplate / WebClient / OkHttp / Apache / Java HttpClient) stay as separate queries — they match method_invocation / object_creation_expression nodes, not annotations, so they can't fold into the same alternation. This change is scoped to the annotation-based route mappers you listed.

I can push it to this branch if useful, or leave it entirely to you — your call. 🙏

magyargergo and others added 3 commits May 30, 2026 06:18
Merge the four annotation pattern bundles (Spring @RequestMapping type
prefix, @FeignClient(path) prefix, @(Get|Post|Put|Delete|Patch)Mapping
method routes and native @RequestLine) into a single
JAVA_ROUTE_ANNOTATION_PATTERNS query, read by scanRouteAnnotations() in
exactly one matches() pass per file. Variants are tagged by branch-local
captures and discriminated in JS (METHOD_ANNOTATION_TO_HTTP,
isRouteMemberKey), per review feedback. This drops the per-file annotation
passes from 4->1 in scan() and 2->1 in collectSpringTypes(), and removes
the interface-@RequestMapping / @FeignClient prefix redundancy.

Verb and path/value key filtering stay in JS rather than in-query: under
the pinned tree-sitter 0.21.1 binding a top-level [...] alternation
compiles to one pattern whose text predicates share a single bucket keyed
by capture name. A #match? against a capture absent from the matched
branch evaluates FALSE and silently drops every sibling-branch match,
whereas #eq? against an absent capture is vacuously true. So only fixed
annotation names use in-query #eq? (on branch-local captures); the
variable verb name and member key carry no in-query predicate.

Behaviour is unchanged for all compilable Java; existing http-route tests
(93) and the full group suite remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in loop

Collapse JAVA_ROUTE_ANNOTATION_PATTERNS from 9 annotation-name-pinned
branches to 6 generic structural branches (class/interface/method x
positional/named) that capture the annotation name (@ann), declaration
(@node), argument (@value) and member key (@key) generically. The query
now carries NO #eq?/#match? predicates at all; scanRouteAnnotations reads
@ann.text and @node.type in its for-loop to decide what each match means
(RequestMapping prefix, FeignClient(path) prefix, @(Get|...)Mapping route,
or @RequestLine), ignoring unrecognised annotations.

This makes the query framework-agnostic and extensible — adding a new
route annotation is a change to the loop and the lookup maps, not the
query — and removes the last tree-sitter-0.21.1 shared-predicate-bucket
footgun, since a predicate-free alternation cannot drop sibling branches.

Behaviour is byte-identical: 93 targeted http-route tests and the full
569-test group suite stay green; tsc and prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…larify invariants

Code-review follow-up to the route-annotation query consolidation. No
behaviour change to the extractor:

- Add two regression tests for branches the generic predicate-free query
  made reachable in scanRouteAnnotations: (1) a @RequestLine whose named
  argument is not `value` must be dropped (the in-query `#eq? @key "value"`
  guard now lives in JS); (2) @FeignClient(path) must win over @RequestMapping
  even when @RequestMapping is the first annotation in source order, covering
  the deferred interfaceRequestMappingPrefixes apply (the existing precedence
  test only covered @FeignClient-first).
- Document two invariants flagged in review: why prefixByTypeId and
  feignPrefixByInterfaceId intentionally diverge for the same interface node
  (Spring provider vs OpenFeign consumer prefix), and that the query's
  single-string-argument shape excludes array-valued annotations.

http-route-extractor + multi-verb suites: 95/95 (was 93); tsc + prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@magyargergo magyargergo merged commit 5d71041 into abhigyanpatwari:main May 30, 2026
26 of 27 checks passed
magyargergo added a commit that referenced this pull request May 30, 2026
…@FeignClient) (#1917)

* fix(group): recognize OpenFeign @RequestLine on plain interfaces (no @FeignClient)

PR #1904 gated @RequestLine consumer extraction on the enclosing
interface also carrying @FeignClient. That guard is wrong: @RequestLine
is a core feign.* annotation used with Feign.builder(), while
@FeignClient is the Spring Cloud variant that uses Spring MVC
annotations (@GetMapping etc.) — the two are effectively mutually
exclusive. Requiring @FeignClient therefore excluded the annotation's
primary, canonical usage, so the feature recognized nothing on real
core-Feign client interfaces.

Fix: drop the @FeignClient requirement for @RequestLine. The match still
requires an enclosing interface (Feign proxies are always interfaces),
and the `RequestLine` annotation name is itself a strong,
framework-specific signal, so false-positive risk stays low. A
@FeignClient(path=...) prefix is still applied when present.

The @(Get|Post|...)Mapping consumer path keeps its @FeignClient
requirement: those annotations are generic Spring MVC and need the Feign
context to be disambiguated from provider routes.

Verification (real-world, not just synthetic fixtures):
- A real client-jar consumer (BigModeClientService.java: a plain
  interface with 12 @RequestLine methods, no @FeignClient) now yields 12
  openfeign consumer contracts; it yielded 0 before this change.
- End-to-end `group sync` over that consumer repo + its FastAPI provider
  repo (with zero hand-written links) produces 12 exact cross-links
  (confidence 1.0), Java @RequestLine consumer → Python route provider.
- The prior test that asserted the wrong behavior
  ("ignores @RequestLine on interfaces without @FeignClient") is
  reversed into a realistic core-Feign fixture.
- Full test/unit/group suite (579) green; tsc and prettier clean.

* test(group): add negative cases for relaxed @RequestLine matcher

Per review on #1917 — guard the no-@FeignClient relaxation with explicit
negative tests: malformed @RequestLine values (no verb / no leading-slash
path / unknown verb) yield no contract, and @RequestLine on a concrete
class method (not an interface) is not emitted as a consumer.

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
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