Skip to content

Fix #1301 - mybase new casing - #1313

Merged
GrahamTheCoder merged 11 commits into
icsharpcode:masterfrom
GrahamTheCoder:claude/fix-1301-mybase-new-casing
Nov 10, 2026
Merged

GrahamTheCoder merged 11 commits into
icsharpcode:masterfrom
GrahamTheCoder:claude/fix-1301-mybase-new-casing

Conversation

@GrahamTheCoder

Copy link
Copy Markdown
Member

Fixes #1301

Summary

Root cause: DeclarationNodeVisitor.VisitConstructorBlock checks whether a constructor's first statement calls another constructor. It decides this from the member name's text being New: in MyBase.New, New is lexed as an identifier. The check failed in three ways, and in each case the call stayed in the body and came out as base.ctor(...); (CS0117/CS7036):

  • Casing: HasMatchingText compared case-sensitively, so MyBase.new(value) and MYBASE.NEW(value) weren't recognised.
  • No parentheses: a parameterless call such as MyBase.New has no InvocationExpressionSyntax, so it was never looked at.
  • Call keyword: Call MyBase.New(value) is a CallStatementSyntax, not an ExpressionStatementSyntax, so it was never looked at either.

Fix:

  • HasMatchingText (CodeConverter/Util/SyntaxTokenExtensions.cs) now compares with OrdinalIgnoreCase:
    • VB keywords are case-insensitive, and the method only takes VB SyntaxKinds.
    • Its only caller is this constructor check, so nothing else is affected.
  • A new GetFirstStatementMemberCall (extracted in a pure-refactor commit) finds the called member access and its arguments in the first statement. It accepts:
    • an expression statement or a Call statement,
    • with or without an invocation around the member access, so with or without parentheses.

The same path handles Me.New and MyClass.New, which become : this(...).

Reviewer attention

  • I kept the syntactic text check and did not switch to a semantic one (GetSymbolInfo resolving to MethodKind.Constructor):
    • In compiling VB, X.New as a statement is only valid when X is MyBase, Me or MyClass, so the text check is exact.
    • An escaped MyBase.[New](...) is not a constructor call: VB reports BC30456. The check ignores it correctly because the token text includes the brackets.
    • The syntactic check keeps working when semantic info is missing, for example an unresolved base type.
  • The first commit on the branch still has its "WIP:" message. I didn't force-push to reword it. Squash-merge would tidy it.

Pre-existing, not fixed

  • If a constructor initializer has a ByRef argument that needs a temporary, the output is : base(ref ph<guid>) (CS0103), for any casing.
    • Example: base Sub New(ByRef r As Integer), derived MyBase.New(value + 1) or MyBase.New(SharedProp).
    • Before this PR, non-canonical casing gave base.ctor(...) in that case, so it was broken either way. It needs a separate fix.

Tests

Tests/CSharp/MemberTests/ConstructorTests.cs has 8 new conversion tests. In all of them both the input and the output compile, since the harness reports no errors.

  • MyBaseNewWithNonCanonicalCasingAsync: the exact repro from the issue.
  • MyBaseNewWithUpperCasingAsync
  • MyBaseNewWithoutParenthesesAsync: MyBase.nEw followed by another statement.
  • MyBaseNewWithoutParenthesesKeepsCommentAsync: the trailing comment is kept.
  • MeAndMyClassNewWithNonCanonicalCasingAsync: Me.new(...) and myclass.NEW(...) become : this(...).
  • CallStatementConstructorInitializersWithNonCanonicalCasingAsync: Call MyBase.new(...), call Me.NEW(...) and CALL myclass.New(...).
  • MyBaseNewWithNonCanonicalCasingNamedAndOmittedArgumentsAsync: named arguments, and an omitted optional argument.
  • FirstStatementNotConstructorCallAsync: Me.Newish(New Base(value)) stays in the body. This is a regression guard and also passes on master.

Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb adds the self-verifying test ConstructorInitializersWithNonCanonicalCasing:

  • It runs the casing, no-parentheses, Me/MyClass chain and Call forms.
  • It asserts which constructors ran and in what order.

Before the fix these tests failed with base.ctor(...) / CS0117. All pass with the fix:

  • On master: the casing, no-parentheses and Call conversion tests, and the self-verifying test.
  • Before the last commit: the Call tests.

Full suite vs master baseline

  • Branch: 1434 passed, 5 skipped, 0 failed (1439).
  • Master baseline: 1425 passed, 5 skipped, 0 failed (1430).
  • The difference is exactly the 9 new tests. No other changes.

Coverage of changed lines

All changed lines are covered, with all branches taken. Two pre-existing, unchanged branches next to the change are still uncovered:

  • IsKindOrHasMatchingText's token.IsKind(NewKeyword) is never true, because Roslyn lexes X.New as an identifier.
  • The final else in VisitConstructorBlock, for <other>.New as the first statement, is invalid in VB.

🤖 Generated with Claude Code

https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg

… and without parentheses

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

The three conversion tests only differed by the blank line the harness
leaves where usings would go (as in the other tests in this file).
Adds coverage for Me.New/MyClass.New with odd casing (this(...)), MyBase.New
without parentheses followed by further statements, a first statement that is
not a constructor call but mentions New, and a self-verifying test that runs
each form and checks which constructors ran.

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

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

Pure refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
"Call MyBase.New(x)" (and Me.New/MyClass.New) as a constructor's first
statement is a CallStatementSyntax rather than an ExpressionStatementSyntax,
so it was converted to "base.ctor(x);" in the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor. The helper now only returns a result when the first statement
really calls another constructor, and arguments are only converted in that
case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
"Me!New()" means Me("New")(), invoking the delegate returned by the default
property. It was converted to ": this()", a self-call (CS0516), or for
"Call Me!New()" a silent behaviour change. Exact casing without Call was
already affected on master; the case-insensitive and Call support widened it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
@GrahamTheCoder
GrahamTheCoder merged commit 044082a into icsharpcode:master Nov 10, 2026
3 checks passed
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.

VB -> C#: Base constructor calls with noncanonical casing produce invalid C#

2 participants