Skip to content

C#: a #if directive between a class name and its brace becomes the class — 48 JsonReader members misattributed #16

Description

@muthuishere

Found by a spike, not by a test

Src/Newtonsoft.Json/JsonReader.Async.cs:

public abstract partial class JsonReader
#if HAVE_ASYNC_DISPOSABLE
    : IAsyncDisposable
#endif
{

The declaration's name is JsonReader. The extractor emits Newtonsoft.HAVE_ASYNC_DISPOSABLE — the preprocessor symbol — and hangs 48 members off it:

class   Newtonsoft.HAVE_ASYNC_DISPOSABLE                     JsonReader.Async.cs L36-L261
method  Newtonsoft.HAVE_ASYNC_DISPOSABLE.DisposeAsync        L42-L53
method  Newtonsoft.HAVE_ASYNC_DISPOSABLE.MoveToContentAsync  L230-L240

declName scans forward from the declaration node for name-typed children, and the #if identifier sits between the class name and the {, so it wins.

Why this matters more than 48 nodes

Those 48 are JsonReader's async surface, attributed to a class that does not exist. So:

  • card JsonReader under-reports its members.
  • affected/change-plan on JsonReader misses everything in the .Async.cs partial.
  • Any query about DisposeAsync / MoveToContentAsync cites a fictional owner.
  • It creates a fake resolution candidate: a spike measuring receiver-type inference kept hitting candidates owned by ['Newtonsoft.HAVE_ASYNC_DISPOSABLE', 'Newtonsoft.JsonTextReader'] — the phantom class competing with the real one, which is exactly the kind of thing that turns a resolvable call into an AMBIGUOUS shortlist.

This corpus is newtonsoft, one of the two pinned judged corpora, and JsonReader's async members being unreachable is a plausible contributor to the low judged score tracked in #11.

Scope, measured

57 of 10,120 nodes on this corpus are named or owned by a SCREAMING_CASE symbol; 48 are this one bug (the rest are legitimate — README, LICENSE, IEEE754, doc sections).

Not measured: how often this shape occurs outside Newtonsoft. C# partial class X #if … : IFoo #endif { is a common conditional-compilation idiom, and the same trap exists anywhere a directive can sit between a declaration's name and its body. Worth checking C/C++ too, where #if between a struct name and brace is legal and common.

Likely fix

declName should take the first name-typed child at the shallowest depth (the declaration's own identifier) rather than scanning past it, or explicitly skip preproc_* node types. tree-sitter's C# grammar exposes preproc_if / preproc_directive as distinct node kinds, so this can be a structural exclusion rather than a heuristic — no guessing about what a name "looks like".

Needs a fixture in internal/golden/testdata/repos/ reproducing the shape, and it will move the newtonsoft corpus node/edge counts (currently pinned at 10120/24130) — a reviewed diff.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions