Skip to content

Write C# member modifiers only where C# allows them [patch] - #225

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/127-csharp-member-modifiers
Oct 14, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/127-csharp-member-modifiers

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #127

What was wrong

CSharpGenerator.WriteFunctionModifiers wrote modifiers without checking what kind of type the member was in. Each of the issue's repros failed to build:

AST Emitted Error
class method, IsReadOnly public readonly int get() CS0106
interface member, IsReadOnly + IsAbstract (CompiledExemplar.Shape()) public abstract readonly double area(); CS0106
IsAbstract member in a Kind = Class type public class Animal { public abstract string speak(); } CS0513
destructor public ~Vec() CS0106 (and CS0575 in a struct)

Change

  • GenerateFunction takes the enclosing ClassDeclaration instead of its name, so WriteFunctionModifiers can see the type's kind.
  • readonly is spelled only on an instance member of a struct (IsReadOnlySpelled). Anywhere else the member still makes the claim, so it gets a note: // readonly: get does not modify its instance. This follows the existing WriteTypePromises handling of a readonly class.
  • abstract: a class with any IsAbstract member is written abstract class (IsAbstractClass). Interface members drop abstract, because a bodiless interface member is already abstract.
  • Destructor: no access modifier. A destructor inside a struct is written as a WriteInexpressible note instead of being declared.

Tests

  • New CSharpMemberModifierTests has seven tests: a class readonly method, struct instance and static methods, CompiledExemplar.Shape() as an interface, abstract and non-abstract classes, and class and struct destructors.
  • FunctionShapeTests.ReadOnly_IsSpelledWhereALanguageHasIt used to pin readonly on a free-standing function, which C# doesn't allow either. It now pins readonly on a struct member, the one place C# allows it.
  • With the generator change reverted, 6 of the 7 new tests fail. The one that still passes is the guard that a class with no abstract member stays non-abstract. With the change, all pass.
  • I also generated all four repros (plus a struct with a readonly method and a destructor) through the changed generator and built the output with dotnet build on net10.0. It succeeded with no errors.
  • Full Coder.Test suite: 1207 passed, 0 failed. dotnet format --verify-no-changes is clean on the touched files.

🤖 Generated with Claude Code

https://claude.ai/code/session_018FP1DMG4WYMABYwTfyb2Xd


Generated by Claude Code

WriteFunctionModifiers wrote the AST's modifiers without looking at the
enclosing type, so ordinary C++-shaped declarations produced C# that
does not compile:

- IsReadOnly became `readonly` on class and interface members (CS0106).
  It is now spelled only on an instance member of a struct; anywhere
  else the claim is written down in a note, as a readonly class's is.
- A destructor got an access modifier (`public ~Vec()`, CS0106). It now
  gets none, and inside a struct, which cannot have a finalizer
  (CS0575), it is written down rather than declared.
- An abstract member left its class unmarked (CS0513). A class with an
  abstract member is now `abstract`, and an interface member drops the
  `abstract` it already has implicitly.

GenerateFunction now takes the enclosing ClassDeclaration rather than
its name so the modifiers can see what kind of type they are in.

Fixes #127

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FP1DMG4WYMABYwTfyb2Xd
Sonar flagged GenerateFunction's cognitive complexity (S3776, 19 of 15)
after the destructor and readonly notes were added to it. They now live
in WriteMemberNotes; the output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FP1DMG4WYMABYwTfyb2Xd
Sonar still scored GenerateFunction at 16 of 15 (S3776) after the
member notes moved out. The initialiser assignments now live in
WriteInitialisers; the output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FP1DMG4WYMABYwTfyb2Xd
…-modifiers

# Conflicts:
#	Coder/Languages/CSharpGenerator.cs
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit ee508e9 into main Oct 14, 2026
15 checks passed
@matt-edmondson
matt-edmondson deleted the fix/127-csharp-member-modifiers branch October 14, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants