Skip to content

fix(extraction): recover C# 14 extension block members - #2231

Open
yasmoradi wants to merge 1 commit into
DeusData:mainfrom
yasmoradi:fix/csharp14-extension-members
Open

yasmoradi wants to merge 1 commit into
DeusData:mainfrom
yasmoradi:fix/csharp14-extension-members

Conversation

@yasmoradi

Copy link
Copy Markdown

Fixes #2071.

What the grammar actually produces

The vendored tree-sitter-c-sharp has no extension production, as noted in the issue thread. Dumping the tree for the reported snippet shows the recovery it falls back on is a constructor_declaration, not a method_declaration:

class_declaration
  identifier                       NumberExtensions
  declaration_list
    constructor_declaration        extension(IEnumerable<int> numbers) { ... }
      identifier                   extension
      parameter_list               (IEnumerable<int> numbers)
      block
        local_function_statement   public int SumPositive() => ...

constructor_declaration is in cs_func_types, so the block is emitted as a Method named extension, and extract_class_methods only walks the direct children of the class body — so the local_function_statement members inside are never reached. has_error stays 0, so nothing surfaces it: not parse_partial, not the #963 coverage flag.

The fix

In extract_class_methods, recognise that shape and emit its members against the enclosing type instead of the block itself.

A constructor carries its own type's name, so extension here can only be a genuine constructor inside a type literally called extension — that is the disambiguation, and it has its own test.

This stays on the pinned grammar. CONTRIBUTING lists vendored dependency changes as needing prior design discussion, so it deliberately does not take the "bump the grammar" route that was the other option offered in the issue thread; that remains open and would supersede this.

Scope, stated plainly

Fixed — the silent case, where nothing tells you anything was lost:

extension(string text)
{
    public int WordCount() => 1;         // now a Method on the enclosing type
    public static string Empty() => "";  // now a Method on the enclosing type
}

Not fixed, and out of scope here:

  • Properties in an extension block. public bool IsBlank => text.Length == 0; parses as ERROR inside the block. Unrecoverable without a grammar that knows the construct — but it does set has_error, so it is at least reported rather than silent.
  • Generic or where-constrained extension blocks. extension<T>(T x) where T : ... { ... } takes a different recovery path: the header becomes an ERROR and the block's { ... } is mistaken for the class body. The members do land in the graph there (with the enclosing type's own body boundaries wrong), and has_error is set, so it is a visibly-degraded parse rather than a silent loss. Worth its own issue if you want it addressed.

Tests

tests/test_extraction.c:

  • csharp_extension_block_members — the members are emitted, and no phantom extension method is.
  • csharp_constructor_of_type_named_extension — a real constructor of a type named extension is still emitted.
make -f Makefile.cbm test-focused TEST_SUITES=extraction

The vendored tree-sitter-c-sharp has no extension production, so an
extension block parses as a constructor_declaration named extension
whose body holds the members as local_function_statement nodes. Nothing
recurses into a constructor body, and has_error stays 0, so the members
were dropped with no signal at all.

Emit them against the enclosing type instead. A constructor carries its
own type name, so this shape is only a genuine constructor inside a type
literally called extension, which keeps its own test.

Fixes DeusData#2071

Signed-off-by: yasmoradi <msynk.dev@gmail.com>
@yasmoradi
yasmoradi requested a review from DeusData as a code owner September 17, 2026 20:57
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Sep 19, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thank you for the focused C# 14 recovery change and for including the genuine constructor-named-extension control alongside the missing-member regression. I have linked this with #2071 and #2218, which addresses the same extraction path with different recovery logic and tests. We need to compare the two approaches during review; neither is being rejected as a duplicate at triage.

Our review queue is currently full, so full review and any merge may take a little time. We appreciate the contribution and the explicitly documented limitations, and will come back with a decision once we have completed that review.

@yasmoradi

Copy link
Copy Markdown
Author

Happy to help! 🙏

@DeusData

Copy link
Copy Markdown
Owner

Thank you for this — and I owe you a heads-up before you invest more time in it.

#2218 by @lorenzozanee fixes the same bug, and defines a function with the same name. Both of you added extract_csharp_extension_members to internal/cbm/extract_defs.c. They cannot both land as they stand — it would be a redefinition — so this needs settling deliberately rather than by whoever merges first. That is our failure for not spotting the overlap earlier; you both filed against a real gap and neither of you could have known.

Reviewing them side by side, your guard is the stronger half, and I want to be specific about why.

Both of you key off a constructor_declaration named extension. #2218 stops there. You go one step further and check the enclosing type's name too:

A constructor carries its own type's name, so this shape can only be a genuine constructor inside a type literally called extension.

That is exactly right, and it is the difference between a heuristic and a safe one. A class genuinely named extension with a genuine constructor is legal C#, and without your check its constructor would be silently reinterpreted as an extension block and its members lifted onto the wrong type. Rare, but wrong-in-silence is the worst failure mode for a graph.

I also appreciate that you documented the limit rather than papering over it — properties in the block parse as ERROR and are beyond recovery without a grammar that knows the construct. Saying so in the comment means the next person does not rediscover it as a mystery.

Where #2218 is ahead: it recurses rather than scanning only the block's direct children, it also accepts method_declaration, and it ships a grammar-label fixture (csharp_extension_declaration with an expected census) that pins the behaviour.

What I would like, if you are both willing: one PR carrying your type-name guard and the recursive traversal and the fixture. I am not going to pick a winner on filing order, and I am not going to quietly close either of you — you have both done good work on the same real bug. If you and @lorenzozanee would rather one of you carries it with the other credited as co-author, that works for me too; tell me which and I will support it.

The vendored grammar having no extension production is the actual root cause, and worth noting: a grammar refresh that adds it would make both of these obsolete. Until then, recovery like yours is the right call.

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

Labels

bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C# 14 extension declarations can cause file to be omitted from index

2 participants