feat(csharp): capture ASP.NET route templates as queryable route nodes - #3248
feat(csharp): capture ASP.NET route templates as queryable route nodes#3248NiSHoW wants to merge 3 commits into
Conversation
_csharp_attribute_names read only an attribute's name, so the argument of
[Route("api/x")] / [HttpGet("Status")] never reached the graph: the extractor
recorded THAT a method is an endpoint (a references[attribute] edge to the
attribute type) but not WHERE it is served. Class-level attributes were not
collected at all, so the controller's route prefix was missing even in
principle, and the full URL could not be recomposed.
The helper now also returns an attribute's first string-literal argument, and
_csharp_route_label composes the endpoint URL: verb from the Http* attribute,
path from that attribute's template or from a sibling [Route] on the same
method (the [HttpGet] + [Route("login")] style that dominates large codebases),
prefixed by the class-level [Route] unless the method template is absolute
(leading / or ~/), with the conventional [controller] token expanded.
The result is a node whose LABEL is the route. That is deliberate: serve.py
indexes (norm_label, label_tokens, nid, source_file, source_tokens) and never
reads node or edge metadata, so a route carried as metadata would be invisible
to `graphify query`. As a node it answers "which controller serves api/x?" with
the existing query/path/explain tooling. The method points at it with
references[context="route"], matching how TS decorators already use a bespoke
context that is not part of REFERENCE_CONTEXTS (that frozenset gates
_semantic_reference_edge, not the AST walk).
Recognition is limited to the ASP.NET routing attributes, so [Obsolete("...")]
and [Display(Name="x")] keep their payload out of the graph. Only C# is
affected: every new helper is _csharp_-prefixed and the emission site sits in
the existing tree_sitter_c_sharp branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A [Route] with no Http* sibling is verb-agnostic in ASP.NET; lock the '*' label so the behaviour is not silently changed later.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds ASP.NET route extraction to the C# engine: methods carrying Http* or Route attributes now emit a route-typed node whose label is "<VERB> <path>", linked back to the handler via a references/route edge so graphify query can find endpoints by URL. Composes a class-level [Route] prefix with the method template (respecting absolute ~/ and / templates), expands the [controller] token from the class name, and labels verb-less [Route] methods with *; non-routing attributes and non-literal arguments mint nothing. Extends _csharp_attribute_names to also return each attribute's first string-literal argument, updating its lone type-reference caller to ignore the new field.
Worth a look
- Named attribute arguments are mistaken for route templates —
graphify/extractors/engine.py:257· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _csharp_attribute_names return arity changed from 3-tuple to 4-tuple —
graphify/extractors/engine.py:268· Escalate · medium · 2 independent checks- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- C# Attribute suffix form is not recognized for routes —
graphify/extractors/engine.py:351· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 662 functions depend on the 236 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_extract_generic()— 18 callers, 25 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - new:
extract_cpp()— 27 callers, 3 callees - new:
extract_vue()— 10 callers, 7 callees - new:
walk()— 1 callers, 58 callees - …and 8 more — each is listed as a finding
Verification — 662 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 602 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_csharp\_attribute\_names.
The verifier did not have enough to check \_csharp\_attribute\_names, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 189 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 16 more finding(s) on lines outside this diff (see the check run).
The fixtures read better when the controller names carry their own meaning: Orders for prefix composition, Products for the [controller] token, Reports for the verb-less [Route]. No behaviour change — the same nine assertions, only the sample code they run against differs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds ASP.NET route extraction to the C# extractor: methods carrying Http*/Route attributes now mint a route-typed node whose label is "<VERB> <path>", linked back to the handler via a references[route] edge. _csharp_route_label composes a class-level [Route] prefix with the method template (respecting absolute ~/ and / templates), expands the [controller] token from the enclosing class name, and falls back to * for a verb-less [Route]; attributes outside the recognized verb set produce no route. Route templates are captured by extending _csharp_attribute_names to carry each attribute's first string-literal argument (a new fourth tuple element that existing callers ignore).
Worth a look
- C# route labels treat named attribute strings as URL templates —
graphify/extractors/engine.py:253· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 662 functions depend on the 236 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_extract_generic()— 18 callers, 25 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - new:
extract_cpp()— 27 callers, 3 callees - new:
extract_vue()— 10 callers, 7 callees - new:
walk()— 1 callers, 58 callees - …and 8 more — each is listed as a finding
Verification — 662 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 602 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_csharp\_attribute\_names.
The verifier did not have enough to check \_csharp\_attribute\_names, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 189 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 16 more finding(s) on lines outside this diff (see the check run).
The gap
For a C# method,
_csharp_attribute_namesreads onlyattr.child_by_field_name("name").The
attribute_argument_listis never touched, so the extractor records thata method is an endpoint — a
references[attribute]edge to the attribute's type —but not where it is served. Class-level attributes are not collected at all,
so a controller's
[Route("api/Orders")]prefix is missing even in principleand the full URL cannot be recomposed from what lands in the graph.
Measured on two production ASP.NET Core repositories (closed source, so names and
endpoints below are redacted; counts and route shapes are verbatim):
[Route(...)]in sourcegraph.jsonRoute)In repo B the graph already knows which 2549 methods carry a
[Route]. Only thestring is missing.
Why a node, and not metadata
serve.py:361indexes(norm_label, label_tokens, nid_text, source, source_tokens)and never reads node or edge
metadata— the threemetadataoccurrences in thatfile are an unrelated comment and an
importlib.metadatacall. A route carried asmetadata would therefore be invisible to
graphify query, which is the whole pointof capturing it. Carried as a node label, it answers "which controller serves
api/x?" with the existing tooling:What it does
_csharp_attribute_namesnow also returns an attribute's first string-literalargument, and
_csharp_route_labelcomposes the URL:Http*attribute; path from that attribute's own template orfrom a sibling
[Route]on the same method — the[HttpGet]+[Route("login")]style is the dominant one in large codebases (1232 bare
[HttpGet]vs 11parameterised, in repo B);
[Route], unless the method template is absolute(leading
/or~/), per ASP.NET's own rule;[controller]token expanded to the class name minus itsControllersuffix;[Route]with no verb sibling is verb-agnostic and labelled*.The method points at the node with
references[context="route"]. That mirrors_ts_emit_decorator_edges, which already uses a bespokecontext="decorator"that is deliberately not part of
REFERENCE_CONTEXTS— that frozenset gates_semantic_reference_edge, not the AST walk — so no vocabulary change is needed.Recognition is limited to the ASP.NET routing attributes, so
[Obsolete("...")]and
[Display(Name="x")]keep their payload out of the graph.Only C# is affected. Every new helper is
_csharp_-prefixed, and the emissionsite sits inside the existing
config.ts_module == "tree_sitter_c_sharp"branch.Validation on real code
Run over the controllers of the two repositories above. Endpoint names are redacted,
but each line preserves the routing construct it exercises:
14/14 for the first repo, matching its method-level attribute count exactly.
Tests
tests/test_csharp_routes.py, 9 tests: template capture, prefix composition,route→handler reachability, the two-attribute style, absolute-template override,
[controller]expansion, node anchoring (source_file+file_type), theverb-less
*case, and a negative test that[Obsolete("...")]mints nothing.Full suite: 5245 passed. The 13 failures on my machine all reproduce on an
unmodified
v8checkout (Windows-only baseline: POSIX FIFO/socket tests, installand hook path tests).
Open question for you
This adds one node per endpoint — about 2900 on a 101k-node graph, ~2.9%. They are
real entities, but they do shift community detection and node counts. I did not add
an opt-out because there is no precedent for gating graph content in this codebase
(every
GRAPHIFY_*variable is operational — models, timeouts, caps, cache, paths)and inventing one felt like a bigger decision than this PR should make. Happy to add
a flag, or to narrow the default, if you would rather it be opt-in.
The same gap exists for Java annotations (
@RequestMapping("/api")) and TSdecorators (
@Component({...})) —_java_annotation_namesand_ts_decorator_nameboth discard arguments by design. If you like this shape I am glad to follow up with
those, one language per PR.
🤖 Generated with Claude Code