Skip to content

Fix: allow ref before partial in struct modifiers - #441

Open
Hartistic wants to merge 1 commit into
tree-sitter:masterfrom
Hartistic:fix/ref-partial-struct
Open

Hartistic wants to merge 1 commit into
tree-sitter:masterfrom
Hartistic:fix/ref-partial-struct

Conversation

@Hartistic

Copy link
Copy Markdown

The bug

C# requires partial to be the last modifier immediately before the type keyword, so ref partial struct S { } is the only legal order for a partial ref struct. The reverse, partial ref struct S { }, is a compiler error (CS1585) — noted in #361 (comment by @sharpchen, 2026-03-21).

_struct_declaration_initializer only recognized a bare optional('ref') sitting after repeat($.modifier):

_struct_declaration_initializer: $ => seq(
  repeat($._attribute_list),
  repeat($.modifier),
  optional('ref'),
  'struct',
  ...

Since partial is a $.modifier, that ordering has it backwards: the C#-illegal partial ref struct parses fine (partial is absorbed by repeat($.modifier), then ref still matches the trailing optional('ref')), while the only legal order, ref partial struct, shreds into an ERROR node — ref is consumed, but the parser has nowhere to put the partial that follows before hitting struct.

Minimal repro

public ref partial struct Reader { } (master, HEAD 9150f7d5, tree-sitter-cli 0.26.5):

(ERROR [0, 0] - [0, 32]
  (modifier [0, 0] - [0, 6])
  (variable_declaration [0, 7] - [0, 25]
    type: (ref_type [0, 7] - [0, 18]
      type: (identifier [0, 11] - [0, 18]))
    (variable_declarator [0, 19] - [0, 25]
      name: (identifier [0, 19] - [0, 25]))))

Same input on this branch:

(struct_declaration [0, 0] - [0, 36]
  (modifier [0, 0] - [0, 6])
  name: (identifier [0, 26] - [0, 32])
  body: (declaration_list [0, 33] - [0, 36]))

Real-world casualty

MessagePack-CSharp's core reader/writer types use exactly this construct:

public ref partial struct MessagePackReader { ... }   // MessagePackReader.cs:27
internal ref partial struct SequenceReader<T> { ... }  // SequenceReader.cs:15

Parsing MessagePackReader.cs on master produces a single ERROR node spanning file rows [21,0]-[1117,5] — the struct's entire ~1,100-line body. On this branch that's gone; the file parses cleanly (the only remaining diagnostics are two unrelated 1-line ERRORs at bare #else/#endif directive tokens from an unconnected preprocessor-conditional-modifier gap that reproduces identically on a plain class — not something this PR touches or introduces).

The fix

One rule, one line, in _struct_declaration_initializer:

-      optional('ref'),
+      optional(seq('ref', optional('partial'))),

ref in every other grammatical position — return types, parameters (ref/ref readonly/scoped ref), locals, lambda parameters — is produced by unrelated rules and is untouched; the existing corpus suite covers all of those (e.g. test/corpus/expressions.txt's "Ref local declaration and ref expression" and "Lambda expression with ref modifier", test/corpus/statements.txt's "Function conditional ref expression", test/corpus/type-methods.txt's ref/scoped ref/ref readonly parameter cases) and all still pass unmodified.

Regenerated src/grammar.json / src/parser.c (and src/tree_sitter/array.h, which drifts along with any regen under this exact pinned CLI version, same as commit f2dea45) with tree-sitter-cli 0.26.5 — matching package-lock.json's pin exactly.

Tests

Added three cases to test/corpus/structs.txt's existing "Struct with a type parameter struct constraint" test, alongside the file's one pre-existing ref struct Test { } case: a bare ref partial struct, public ref partial struct MessagePackReader (the real-world construct verbatim), and internal ref partial struct SequenceReader<T> (real-world construct plus a type parameter). Full suite: 179/179 corpus tests pass on both master and this branch (same total — this extends an existing test rather than adding a new title), zero new failures; npm test and npm run lint both clean.

Fixes #361

🤖 Generated with Claude Code

C# requires `partial` to be the last modifier immediately before the
type keyword, so `ref partial struct S { }` is the only legal order —
the reverse, `partial ref struct S { }`, is a compiler error (CS1585).
`_struct_declaration_initializer` only recognized a bare `optional('ref')`
after the modifier repeat, so the legal order shredded into an ERROR
node while the illegal order happened to parse (`partial` fell inside
`repeat($.modifier)` and `ref` still matched the trailing optional).

This broke real-world code: MessagePack-CSharp's core reader/writer
types are declared `public ref partial struct MessagePackReader` /
`internal ref partial struct SequenceReader<T>`, and the ERROR
previously swallowed the struct's entire ~1,100-line body.

Fixes tree-sitter#361

Change `optional('ref')` to `optional(seq('ref', optional('partial')))`
in `_struct_declaration_initializer`. `ref` in every other position
(returns, parameters, locals, lambda parameters) is untouched — it's
produced by unrelated rules and the existing corpus tests for all of
those still pass unmodified.

Regenerated with tree-sitter-cli 0.26.5, matching package-lock.json's
pin. Added `ref partial struct` corpus cases (bare, with an access
modifier, and with a type parameter) to test/corpus/structs.txt,
alongside the existing `ref struct` case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

struct's modifiers are too sensitive to order

1 participant