Skip to content

Fill four .NET (C# + Razor) extraction gaps#1869

Open
papinto wants to merge 4 commits into
Graphify-Labs:v8from
papinto:fix/dotnet-extractor-gaps
Open

Fill four .NET (C# + Razor) extraction gaps#1869
papinto wants to merge 4 commits into
Graphify-Labs:v8from
papinto:fix/dotnet-extractor-gaps

Conversation

@papinto

@papinto papinto commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Four independent .NET extraction gaps where a real type reference produced no edge — or a dangling one that got pruned — so a defined type was left disconnected from the code that uses it. Two are in the C# extractor, two in the Razor extractor. Each is a self-contained commit with its own regression test, so they can be taken together or cherry-picked individually.

# Construct Before After
1 C# delegate type no node at all → references to it dangle first-class type node, references resolve
2 C# generic field Dictionary<Widget, _> edge to Dictionary only + generic_arg edge to Widget
3 Razor @model/@inherits/@inject sourced node labeled with the full dotted string, never matches the class sourceless simple-name stub that collapses onto the real class
4 Razor @using Alias = Qualified.Type sourced decoy per view that blocks stub rewire sourceless stub for the aliased type

1 — C# delegate declarations as nodes

The C# class_types set treats class/interface/enum/struct/record as type definitions but omits delegate_declaration. A named delegate type such as public delegate int Handler(string input) therefore produced no node at all — and any parameter, field, or return typed as that delegate referenced a node that did not exist, so the edge dangled and was pruned. Delegate types were invisible to the graph and to definition→usage tracing.

Fix: add delegate_declaration to the C# class_types. A delegate has a name field (so the node is labeled correctly) but no declaration_list body, so no members are walked — just the type node and the file contains edge. Existing references to the delegate now resolve to it instead of dangling.

2 — C# field generic type-argument references

The C# field_declaration handler read a single outer type name, so a field like Dictionary<Widget, string> _registry produced a references edge to Dictionary but none to Widget. Generic type-argument couplings on fields were invisible, breaking definition→usage traceability.

Fix: walk the field type with _csharp_collect_type_refs — the same helper the parameter/return-type paths and the C# property handler already use — so the outer type keeps field context while each generic argument is emitted with generic_arg context. This simply brings fields to parity with the property handler directly below them. _read_csharp_type_name is retained; it is still used by the base-type and local-declaration paths.

3 — Razor @model / @inherits / @inject resolve to the real class

These directives recorded their target as a sourced node labeled with the full directive text, e.g. @model Some.Deep.Namespace.MyViewModel became a node labeled with that entire dotted string. That never matched the real class node (label MyViewModel, defined in another file): the corpus-level _rewire_unique_stub_nodes only collapses sourceless stubs whose simple label matches exactly one real type definition, and a label containing a dot is rejected as type-like. So .cshtml/.razor views were left unlinked from the view models they bind.

Fix: record @model/@inherits/@inject the way the C# extractor records cross-file type refs — the simple type name (namespace, generics, and array suffix stripped) on a sourceless stub — so the existing rewire collapses it onto the real class. @using (a namespace import, not a type reference) is unchanged.

4 — Razor @using type-aliases resolve to the aliased type

A using-alias directive @using Alias = Qualified.Type names a type, not a namespace, but the @using handler matched it with the plain-namespace regex and recorded the alias name as a sourced node per view. Those sourced, simple-name, type-like nodes are decoys: because the rewire only collapses a stub when exactly one sourced definition of that label exists, N views aliasing the same type made the label ambiguous, so a real consumer's stub never collapsed onto the real class — the target sat disconnected from its usages.

Fix: detect the alias form first and resolve it to the aliased (right-hand-side) type as a sourceless simple-name stub — the same treatment as @model/@inherits/@inject — so the corpus rewire collapses it onto the real class and the per-view decoys disappear. Plain namespace imports @using Ns.Sub are unchanged (still a sourced namespace node).

Tests

  • C# (tests/test_languages.py, tests/fixtures/sample.cs): a delegate and a generic field added to the fixture; a test that the delegate becomes a node, and a test that a generic field emits both the outer field ref and the generic_arg ref. The generic-field test is verified to fail without the fix.
  • Razor (tests/test_dotnet.py): unit tests for the sourceless simple-name stub (@model and @using-alias), a test that the alias resolves to the RHS type rather than the alias name, a test that plain @using namespace imports keep their prior sourced behavior, and end-to-end tests that the stubs collapse onto the real class via _rewire_unique_stub_nodes. All verified to fail without the fixes.

The existing C#, Java, Razor, and cross-file symbol-resolution suites remain green.

Notes

  • Fixes 1–2 are gated on the C# grammar and fix 2 reuses an existing helper, so no other language's extraction changes. Fixes 3–4 touch only Razor directive handling and rely on the existing stub-rewire pass — no new resolution machinery.
  • Each commit stands alone; order within the branch is the two C# fixes followed by the two Razor fixes.

papinto and others added 4 commits July 13, 2026 14:04
The C# extractor recognized only class/interface/enum/struct/record as type
definitions, so `delegate_declaration` was never emitted as a node -- a named
delegate type like `delegate int Handler(...)` had no node at all. Any
parameter, field, or return typed as that delegate referenced a non-existent
node, so the edge dangled and was pruned: delegate types were invisible to the
graph and to definition->usage tracing.

Add `delegate_declaration` to the C# `class_types`. A delegate has a `name`
field (so the node is labeled correctly) but no `declaration_list` body, so no
members are walked -- just the type node and the file `contains` edge, and
existing references to the delegate now resolve to it instead of dangling.

Adds a regression test and a delegate to the C# fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The C# `field_declaration` handler read a single outer type name via
_read_csharp_type_name, so a field like `Dictionary<Widget, string>` produced a
`references` edge to the collection type but none to its type arguments. Generic
type-argument couplings were therefore invisible in the graph, breaking
definition->usage traceability.

Walk the field type with _csharp_collect_type_refs -- the same helper the
parameter/return-type paths and the C# property handler already use -- so each
generic argument is emitted with generic_arg context while the outer type keeps
field context. This mirrors the property handler directly below it;
_read_csharp_type_name is still used by the base-type and local-declaration
paths, so it is left in place.

Adds a regression test (generic field -> generic_arg edge) and a generic field
to the C# fixture; verified the test fails without the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l classes

Razor type-reference directives recorded their target as a SOURCED node labeled
with the full directive text, e.g. `@model Some.Deep.Namespace.MyViewModel` became
a node labeled with that whole dotted string. That never matched the real class
node (label `MyViewModel`, defined in another file): the corpus-level
_rewire_unique_stub_nodes only collapses SOURCELESS stubs whose SIMPLE label
matches exactly one real type definition, and _is_type_like_definition rejects any
label containing a dot. So .cshtml/.razor views were left unlinked to the view
models they bind.

Record @model/@inherits/@Inject like the C# extractor records cross-file type
refs: the simple type name (namespace/generics/array stripped) on a sourceless
stub, so the existing rewire collapses it onto the real class. @using (a namespace
import, not a type) is unchanged.

Adds a unit test (sourceless simple-name stub) and an integration test (stub
rewired onto the real class); both verified to fail without the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A using-alias directive `@using Alias = Qualified.Type` names a TYPE, not a
namespace, but the `@using` handler matched it with the plain-namespace regex and
recorded the ALIAS name (`@using Foo = Some.Namespace.Widget` captured `Foo`) as a
SOURCED node per view. Those sourced, simple-name, type-like nodes are decoys:
_rewire_unique_stub_nodes only collapses a stub onto a real definition when exactly
ONE sourced def of that label exists, so N views aliasing the same type made the
label ambiguous and the real consumer stub never collapsed onto the real class.
The target node sat at degree 1, disconnected from its usages.

Detect the alias form first and resolve it to the ALIASED (RHS) type as a
sourceless simple-name stub (the same treatment as @model/@inherits/@Inject), so
the corpus rewire collapses it onto the real class and the decoys vanish. Plain
namespace imports `@using Ns.Sub` are unchanged (still a sourced namespace node).

Adds unit tests (sourceless aliased-type stub; RHS-not-alias naming; plain
namespace unchanged) and an integration test reproducing the decoys-block-rewire
regression; all verified to fail without the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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.

1 participant