From 84caaff1495c710de00d0dd01be51a17efa9132a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:26:53 +0000 Subject: [PATCH 01/11] WIP: recognise MyBase.New constructor initializers case-insensitively and without parentheses Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/DeclarationNodeVisitor.cs | 6 +- CodeConverter/Util/SyntaxTokenExtensions.cs | 3 +- Tests/CSharp/MemberTests/ConstructorTests.cs | 108 ++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 74a905a13..72b8a435a 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -949,8 +949,10 @@ public override async Task VisitConstructorBlock(VBSyntax.Cons var attributes = await block.AttributeLists.SelectManyAsync(CommonConversions.ConvertAttributeAsync); var modifiers = CommonConversions.ConvertModifiers(block, block.Modifiers, node.GetMemberContext()); - var ctor = (node.Statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression as VBSyntax.InvocationExpressionSyntax; - var ctorExpression = ctor?.Expression as VBSyntax.MemberAccessExpressionSyntax; + // Parentheses are optional when calling a parameterless constructor, so "MyBase.New" has no invocation around it + var firstExpression = (node.Statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression; + var ctor = firstExpression as VBSyntax.InvocationExpressionSyntax; + var ctorExpression = (ctor?.Expression ?? firstExpression) as VBSyntax.MemberAccessExpressionSyntax; var ctorArgs = await (ctor?.ArgumentList).AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ArgumentList(); IEnumerable statements; diff --git a/CodeConverter/Util/SyntaxTokenExtensions.cs b/CodeConverter/Util/SyntaxTokenExtensions.cs index 6fcefdb2f..db59a68a2 100644 --- a/CodeConverter/Util/SyntaxTokenExtensions.cs +++ b/CodeConverter/Util/SyntaxTokenExtensions.cs @@ -23,7 +23,8 @@ public static bool IsKindOrHasMatchingText(this SyntaxToken token, Microsoft.Cod public static bool HasMatchingText(this SyntaxToken token, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind kind) { - return token.ToString() == VBasic.SyntaxFacts.GetText(kind); + // VB is case insensitive, and e.g. "MyBase.new" is lexed as an identifier rather than the New keyword + return string.Equals(token.ToString(), VBasic.SyntaxFacts.GetText(kind), StringComparison.OrdinalIgnoreCase); } public static bool IsKind(this SyntaxToken token, SyntaxKind kind1, SyntaxKind kind2) diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index 3de4d01e9..91d509d37 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -218,6 +218,114 @@ public StaticLocalConvertedToField(int x) { Console.WriteLine(_sPrevPosition1); } +}"); + } + + [Fact] + public async Task MyBaseNewWithNonCanonicalCasingAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New() + End Sub + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + MyBase.new(value) + End Sub +End Class", @"public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) : base(value) + { + } +}"); + } + + [Fact] + public async Task MyBaseNewWithUpperCasingAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New() + End Sub + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + MYBASE.NEW(value) + End Sub +End Class", @"public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) : base(value) + { + } +}"); + } + + [Fact] + public async Task MyBaseNewWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New() + End Sub + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + MyBase.nEw + End Sub +End Class", @"public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) : base() + { + } }"); } } \ No newline at end of file From 710657da82406bda404eb2588f184392f25d0ba1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:54:43 +0000 Subject: [PATCH 02/11] Fix expectations and extend tests for non-canonically cased constructor 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 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- Tests/CSharp/MemberTests/ConstructorTests.cs | 109 +++++++++++++++++- .../VBToCS/InheritanceTests.vb | 42 +++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index 91d509d37..3bad5206a 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -238,7 +238,8 @@ Inherits Base Public Sub New(value As String) MyBase.new(value) End Sub -End Class", @"public partial class Base +End Class", @" +public partial class Base { public Base() { @@ -274,7 +275,8 @@ Inherits Base Public Sub New(value As String) MYBASE.NEW(value) End Sub -End Class", @"public partial class Base +End Class", @" +public partial class Base { public Base() { @@ -309,8 +311,11 @@ Inherits Base Public Sub New(value As String) MyBase.nEw + Console.WriteLine(value) End Sub -End Class", @"public partial class Base +End Class", @"using System; + +public partial class Base { public Base() { @@ -324,6 +329,104 @@ public partial class Derived : Base { public Derived(string value) : base() + { + Console.WriteLine(value); + } +}"); + } + + [Fact] + public async Task MeAndMyClassNewWithNonCanonicalCasingAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New() + End Sub + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New() + Me.new(""a"") + End Sub + Public Sub New(value As Integer) + myclass.NEW(value.ToString()) + Console.WriteLine(value) + End Sub + Public Sub New(value As String) + MyBase.New(value) + End Sub +End Class", @"using System; + +public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived() : this(""a"") + { + } + public Derived(int value) : this(value.ToString()) + { + Console.WriteLine(value); + } + public Derived(string value) : base(value) + { + } +}"); + } + + [Fact] + public async Task FirstStatementNotConstructorCallAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New() + End Sub + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + Me.Newish(New Base(value)) + MyBase.ToString() + End Sub + Private Sub Newish(b As Base) + End Sub +End Class", @" +public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) + { + Newish(new Base(value)); + base.ToString(); + } + private void Newish(Base b) { } }"); diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb index b51d5ed64..d11e546a7 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb @@ -10,6 +10,48 @@ Module Program Dim x As A = New B Assert.True(x.TestMethod()) End Sub + + + Public Sub ConstructorInitializersWithNonCanonicalCasing() + Assert.Equal("Base(base)", New NonCanonicalCtorCalls("base").Log) + Assert.Equal("Base(me);Derived(me)", New NonCanonicalCtorCalls(1).Log) + Assert.Equal("Base(myclass);Derived(myclass)", New NonCanonicalCtorCalls(1L).Log) + Assert.Equal("Base();Derived(short)", New NonCanonicalCtorCalls(1S).Log) + Assert.Equal("Base(me);Derived(me);Derived()", New NonCanonicalCtorCalls().Log) + End Sub + End Class + + Public Class CtorBase + Public Log As String + Public Sub New() + Log = "Base()" + End Sub + Public Sub New(value As String) + Log = "Base(" & value & ")" + End Sub + End Class + + Public Class NonCanonicalCtorCalls + Inherits CtorBase + Public Sub New(value As String) + MyBase.new(value) + End Sub + Public Sub New(value As Integer) + MYBASE.NEW("me") + Log &= ";Derived(me)" + End Sub + Public Sub New(value As Long) + myclass.New("myclass") + Log &= ";Derived(myclass)" + End Sub + Public Sub New(value As Short) + mybase.nEw + Log &= ";Derived(short)" + End Sub + Public Sub New() + Me.new(1) + Log &= ";Derived()" + End Sub End Class Public Interface IA From 46f52f4862cb6ec7235e2534b2ca33cf04a86dbb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:36:14 +0000 Subject: [PATCH 03/11] Test comments, named and omitted arguments on non-canonically cased MyBase.New Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- Tests/CSharp/MemberTests/ConstructorTests.cs | 65 ++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index 3bad5206a..ddd0e0c2f 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -429,6 +429,71 @@ public Derived(string value) private void Newish(Base b) { } +}"); + } + + [Fact] + public async Task MyBaseNewWithoutParenthesesKeepsCommentAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base +End Class + +Public Class Derived + Inherits Base + + Public Sub New() + MyBase.nEw ' Comment + End Sub +End Class", @" +public partial class Base +{ +} + +public partial class Derived : Base +{ + + public Derived() : base() // Comment + { + } +}"); + } + + [Fact] + public async Task MyBaseNewWithNonCanonicalCasingNamedAndOmittedArgumentsAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New(value As String, Optional n As Integer = 3) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + MyBase.new(n:=4, value:=value) + End Sub + Public Sub New(value As Integer) + mybase.New(value.ToString(), ) + End Sub +End Class", @" +public partial class Base +{ + public Base(string value, int n = 3) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) : base(n: 4, value: value) + { + } + public Derived(int value) : base(value.ToString()) + { + } }"); } } \ No newline at end of file From 3213e008c6d2f07649503be145a51f2dad15b7e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:36:48 +0000 Subject: [PATCH 04/11] Extract finding the constructor call from a constructor's first statement Pure refactor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/DeclarationNodeVisitor.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 72b8a435a..0bccd4540 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -949,11 +949,8 @@ public override async Task VisitConstructorBlock(VBSyntax.Cons var attributes = await block.AttributeLists.SelectManyAsync(CommonConversions.ConvertAttributeAsync); var modifiers = CommonConversions.ConvertModifiers(block, block.Modifiers, node.GetMemberContext()); - // Parentheses are optional when calling a parameterless constructor, so "MyBase.New" has no invocation around it - var firstExpression = (node.Statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression; - var ctor = firstExpression as VBSyntax.InvocationExpressionSyntax; - var ctorExpression = (ctor?.Expression ?? firstExpression) as VBSyntax.MemberAccessExpressionSyntax; - var ctorArgs = await (ctor?.ArgumentList).AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ArgumentList(); + var (ctorExpression, ctorVbArgs) = GetFirstStatementMemberCall(node.Statements); + var ctorArgs = await ctorVbArgs.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ArgumentList(); IEnumerable statements; ConstructorInitializerSyntax ctorCall; @@ -982,6 +979,17 @@ await block.ParameterList.AcceptAsync(_triviaConvertingExpr ); } + /// + /// VB has no constructor initializer syntax: a call to another constructor (MyBase.New, Me.New or MyClass.New) can only be the first statement. + /// Parentheses are optional when calling a parameterless constructor, so "MyBase.New" may have no invocation around it. + /// + private static (VBSyntax.MemberAccessExpressionSyntax Target, VBSyntax.ArgumentListSyntax Arguments) GetFirstStatementMemberCall(SyntaxList statements) + { + var calledExpression = (statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression; + var invocation = calledExpression as VBSyntax.InvocationExpressionSyntax; + return ((invocation?.Expression ?? calledExpression) as VBSyntax.MemberAccessExpressionSyntax, invocation?.ArgumentList); + } + public override async Task VisitDeclareStatement(VBSyntax.DeclareStatementSyntax node) { var importAttributes = new List(); From 8ea0f2e28a3a9a80a7ebf98e40ad653f9c4c6830 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:41:17 +0000 Subject: [PATCH 05/11] Recognise constructor calls written with the Call keyword "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 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/DeclarationNodeVisitor.cs | 7 ++- Tests/CSharp/MemberTests/ConstructorTests.cs | 44 +++++++++++++++++++ .../VBToCS/InheritanceTests.vb | 15 +++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 0bccd4540..823b39b0a 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -982,10 +982,15 @@ await block.ParameterList.AcceptAsync(_triviaConvertingExpr /// /// VB has no constructor initializer syntax: a call to another constructor (MyBase.New, Me.New or MyClass.New) can only be the first statement. /// Parentheses are optional when calling a parameterless constructor, so "MyBase.New" may have no invocation around it. + /// The call may also be written with the Call keyword, e.g. "Call MyBase.New(x)". /// private static (VBSyntax.MemberAccessExpressionSyntax Target, VBSyntax.ArgumentListSyntax Arguments) GetFirstStatementMemberCall(SyntaxList statements) { - var calledExpression = (statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression; + var calledExpression = statements.FirstOrDefault() switch { + VBSyntax.ExpressionStatementSyntax expressionStatement => expressionStatement.Expression, + VBSyntax.CallStatementSyntax callStatement => callStatement.Invocation, + _ => null + }; var invocation = calledExpression as VBSyntax.InvocationExpressionSyntax; return ((invocation?.Expression ?? calledExpression) as VBSyntax.MemberAccessExpressionSyntax, invocation?.ArgumentList); } diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index ddd0e0c2f..97d4c7b09 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -494,6 +494,50 @@ public Derived(string value) : base(n: 4, value: value) public Derived(int value) : base(value.ToString()) { } +}"); + } + + [Fact] + public async Task CallStatementConstructorInitializersWithNonCanonicalCasingAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Base + Public Sub New(value As String) + End Sub +End Class + +Public Class Derived + Inherits Base + + Public Sub New(value As String) + Call MyBase.new(value) + End Sub + Public Sub New(value As Integer) + call Me.NEW(value.ToString()) + End Sub + Public Sub New() + CALL myclass.New(1) + End Sub +End Class", @" +public partial class Base +{ + public Base(string value) + { + } +} + +public partial class Derived : Base +{ + + public Derived(string value) : base(value) + { + } + public Derived(int value) : this(value.ToString()) + { + } + public Derived() : this(1) + { + } }"); } } \ No newline at end of file diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb index d11e546a7..815d7acc1 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb @@ -18,6 +18,9 @@ Module Program Assert.Equal("Base(myclass);Derived(myclass)", New NonCanonicalCtorCalls(1L).Log) Assert.Equal("Base();Derived(short)", New NonCanonicalCtorCalls(1S).Log) Assert.Equal("Base(me);Derived(me);Derived()", New NonCanonicalCtorCalls().Log) + Assert.Equal("Base(call);Derived(call)", New NonCanonicalCtorCalls(1.0).Log) + Assert.Equal("Base(me);Derived(me);Derived(call me)", New NonCanonicalCtorCalls(1D).Log) + Assert.Equal("Base(myclass);Derived(myclass);Derived(call myclass)", New NonCanonicalCtorCalls(CByte(1)).Log) End Sub End Class @@ -52,6 +55,18 @@ Module Program Me.new(1) Log &= ";Derived()" End Sub + Public Sub New(value As Double) + Call MyBase.new("call") + Log &= ";Derived(call)" + End Sub + Public Sub New(value As Decimal) + CALL ME.NEW(1) + Log &= ";Derived(call me)" + End Sub + Public Sub New(value As Byte) + call myclass.New(1L) + Log &= ";Derived(call myclass)" + End Sub End Class Public Interface IA From 8f1f3794aecf4afb95a697eb1404bb962773fe9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:08:18 +0000 Subject: [PATCH 06/11] Fold the constructor-call check into GetConstructorInitializerCall 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 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/DeclarationNodeVisitor.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 823b39b0a..823451126 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -949,23 +949,12 @@ public override async Task VisitConstructorBlock(VBSyntax.Cons var attributes = await block.AttributeLists.SelectManyAsync(CommonConversions.ConvertAttributeAsync); var modifiers = CommonConversions.ConvertModifiers(block, block.Modifiers, node.GetMemberContext()); - var (ctorExpression, ctorVbArgs) = GetFirstStatementMemberCall(node.Statements); - var ctorArgs = await ctorVbArgs.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ArgumentList(); - - IEnumerable statements; - ConstructorInitializerSyntax ctorCall; - if (ctorExpression == null || !ctorExpression.Name.Identifier.IsKindOrHasMatchingText(VBasic.SyntaxKind.NewKeyword)) { - statements = node.Statements; - ctorCall = null; - } else if (ctorExpression.Expression is VBSyntax.MyBaseExpressionSyntax) { + IEnumerable statements = node.Statements; + ConstructorInitializerSyntax ctorCall = null; + if (GetConstructorInitializerCall(node.Statements) is var (initializerKind, vbArguments)) { + var ctorArgs = await vbArguments.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ArgumentList(); + ctorCall = CS.SyntaxFactory.ConstructorInitializer(initializerKind, ctorArgs); statements = node.Statements.Skip(1); - ctorCall = CS.SyntaxFactory.ConstructorInitializer(CS.SyntaxKind.BaseConstructorInitializer, ctorArgs); - } else if (ctorExpression.Expression is VBSyntax.MeExpressionSyntax || ctorExpression.Expression is VBSyntax.MyClassExpressionSyntax) { - statements = node.Statements.Skip(1); - ctorCall = CS.SyntaxFactory.ConstructorInitializer(CS.SyntaxKind.ThisConstructorInitializer, ctorArgs); - } else { - statements = node.Statements; - ctorCall = null; } var convertedBodyStatements = await ConvertMethodBodyStatementsAsync(node, statements.ToArray()); @@ -984,7 +973,8 @@ await block.ParameterList.AcceptAsync(_triviaConvertingExpr /// Parentheses are optional when calling a parameterless constructor, so "MyBase.New" may have no invocation around it. /// The call may also be written with the Call keyword, e.g. "Call MyBase.New(x)". /// - private static (VBSyntax.MemberAccessExpressionSyntax Target, VBSyntax.ArgumentListSyntax Arguments) GetFirstStatementMemberCall(SyntaxList statements) + /// null when the first statement doesn't call another constructor + private static (CS.SyntaxKind InitializerKind, VBSyntax.ArgumentListSyntax Arguments)? GetConstructorInitializerCall(SyntaxList statements) { var calledExpression = statements.FirstOrDefault() switch { VBSyntax.ExpressionStatementSyntax expressionStatement => expressionStatement.Expression, @@ -992,7 +982,17 @@ private static (VBSyntax.MemberAccessExpressionSyntax Target, VBSyntax.ArgumentL _ => null }; var invocation = calledExpression as VBSyntax.InvocationExpressionSyntax; - return ((invocation?.Expression ?? calledExpression) as VBSyntax.MemberAccessExpressionSyntax, invocation?.ArgumentList); + if ((invocation?.Expression ?? calledExpression) is not VBSyntax.MemberAccessExpressionSyntax memberAccess + || !memberAccess.Name.Identifier.IsKindOrHasMatchingText(VBasic.SyntaxKind.NewKeyword)) { + return null; + } + + CS.SyntaxKind? initializerKind = memberAccess.Expression switch { + VBSyntax.MyBaseExpressionSyntax => CS.SyntaxKind.BaseConstructorInitializer, + VBSyntax.MeExpressionSyntax or VBSyntax.MyClassExpressionSyntax => CS.SyntaxKind.ThisConstructorInitializer, + _ => null + }; + return initializerKind is { } kind ? (kind, invocation?.ArgumentList) : null; } public override async Task VisitDeclareStatement(VBSyntax.DeclareStatementSyntax node) From 2b8bd69bc6966c984a3c2d034d7ebfe1c89d6b6a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:08:27 +0000 Subject: [PATCH 07/11] Clarify why HasMatchingText ignores case Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/Util/SyntaxTokenExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeConverter/Util/SyntaxTokenExtensions.cs b/CodeConverter/Util/SyntaxTokenExtensions.cs index db59a68a2..5082e7c40 100644 --- a/CodeConverter/Util/SyntaxTokenExtensions.cs +++ b/CodeConverter/Util/SyntaxTokenExtensions.cs @@ -23,7 +23,7 @@ public static bool IsKindOrHasMatchingText(this SyntaxToken token, Microsoft.Cod public static bool HasMatchingText(this SyntaxToken token, Microsoft.CodeAnalysis.VisualBasic.SyntaxKind kind) { - // VB is case insensitive, and e.g. "MyBase.new" is lexed as an identifier rather than the New keyword + // A keyword after "." (e.g. "MyBase.New") is lexed as an identifier token, and VB is case insensitive return string.Equals(token.ToString(), VBasic.SyntaxFacts.GetText(kind), StringComparison.OrdinalIgnoreCase); } From 6db142ea6f6ae643b5607008037956a1def12f0f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:09:30 +0000 Subject: [PATCH 08/11] Test that a parenless Call to a non-constructor method stays in the body Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- Tests/CSharp/MemberTests/ConstructorTests.cs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index 97d4c7b09..d3184fe38 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -538,6 +538,29 @@ public Derived(int value) : this(value.ToString()) public Derived() : this(1) { } +}"); + } + + [Fact] + public async Task CallStatementNonConstructorWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Derived + Public Sub New() + Call Me.Foo + End Sub + Private Sub Foo() + End Sub +End Class", @" +public partial class Derived +{ + public Derived() + { + Foo(); + } + private void Foo() + { + } }"); } } \ No newline at end of file From 0266d0c267cf55cbbf02d4a6dbf6a5543c6df268 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:11:18 +0000 Subject: [PATCH 09/11] Don't treat dictionary access Me!New as a constructor call "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 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/DeclarationNodeVisitor.cs | 2 ++ Tests/CSharp/MemberTests/ConstructorTests.cs | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 823451126..3bd587b5e 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -982,7 +982,9 @@ private static (CS.SyntaxKind InitializerKind, VBSyntax.ArgumentListSyntax Argum _ => null }; var invocation = calledExpression as VBSyntax.InvocationExpressionSyntax; + // "Me!New" is dictionary access (Me("New")) rather than a constructor call if ((invocation?.Expression ?? calledExpression) is not VBSyntax.MemberAccessExpressionSyntax memberAccess + || !memberAccess.IsKind(VBasic.SyntaxKind.SimpleMemberAccessExpression) || !memberAccess.Name.Identifier.IsKindOrHasMatchingText(VBasic.SyntaxKind.NewKeyword)) { return null; } diff --git a/Tests/CSharp/MemberTests/ConstructorTests.cs b/Tests/CSharp/MemberTests/ConstructorTests.cs index d3184fe38..1c9a622e0 100644 --- a/Tests/CSharp/MemberTests/ConstructorTests.cs +++ b/Tests/CSharp/MemberTests/ConstructorTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using ICSharpCode.CodeConverter.CSharp; using ICSharpCode.CodeConverter.Tests.TestRunners; using Xunit; @@ -563,4 +564,30 @@ private void Foo() } }"); } + + [Fact] + public async Task DictionaryAccessNamedNewIsNotConstructorCallAsync() + { + // The body conversion drops the invocation of the dictionary-accessed delegate (CS0201), so only the constructor initializer is asserted on + var convertedCode = await ConvertAsync(@"Imports System + +Public Class Derived + Default ReadOnly Property Item(k As String) As Action + Get + Return Sub() Console.WriteLine(k) + End Get + End Property + + Public Sub New() + Me!new() + End Sub + Public Sub New(value As String) + Call Me!New() + End Sub +End Class"); + + Assert.DoesNotContain(": this(", convertedCode); + Assert.Contains("this[\"new\"]", convertedCode); + Assert.Contains("this[\"New\"]", convertedCode); + } } \ No newline at end of file From f9038101ec9812325fb05d048b327367be419953 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 22:21:45 +0000 Subject: [PATCH 10/11] Return the constructor initializer call directly from the switch Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/DeclarationNodeVisitor.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 3bd587b5e..e942dee50 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -989,12 +989,11 @@ private static (CS.SyntaxKind InitializerKind, VBSyntax.ArgumentListSyntax Argum return null; } - CS.SyntaxKind? initializerKind = memberAccess.Expression switch { - VBSyntax.MyBaseExpressionSyntax => CS.SyntaxKind.BaseConstructorInitializer, - VBSyntax.MeExpressionSyntax or VBSyntax.MyClassExpressionSyntax => CS.SyntaxKind.ThisConstructorInitializer, + return memberAccess.Expression switch { + VBSyntax.MyBaseExpressionSyntax => (CS.SyntaxKind.BaseConstructorInitializer, invocation?.ArgumentList), + VBSyntax.MeExpressionSyntax or VBSyntax.MyClassExpressionSyntax => (CS.SyntaxKind.ThisConstructorInitializer, invocation?.ArgumentList), _ => null }; - return initializerKind is { } kind ? (kind, invocation?.ArgumentList) : null; } public override async Task VisitDeclareStatement(VBSyntax.DeclareStatementSyntax node) From 384674ed6e91d648399dce376908dc2b414cc1cc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 22:23:56 +0000 Subject: [PATCH 11/11] Make self-verifying constructor test log labels describe each call form Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../VBToCS/InheritanceTests.vb | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb index 815d7acc1..fc19e9fac 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb @@ -13,14 +13,14 @@ Module Program Public Sub ConstructorInitializersWithNonCanonicalCasing() - Assert.Equal("Base(base)", New NonCanonicalCtorCalls("base").Log) - Assert.Equal("Base(me);Derived(me)", New NonCanonicalCtorCalls(1).Log) - Assert.Equal("Base(myclass);Derived(myclass)", New NonCanonicalCtorCalls(1L).Log) - Assert.Equal("Base();Derived(short)", New NonCanonicalCtorCalls(1S).Log) - Assert.Equal("Base(me);Derived(me);Derived()", New NonCanonicalCtorCalls().Log) - Assert.Equal("Base(call);Derived(call)", New NonCanonicalCtorCalls(1.0).Log) - Assert.Equal("Base(me);Derived(me);Derived(call me)", New NonCanonicalCtorCalls(1D).Log) - Assert.Equal("Base(myclass);Derived(myclass);Derived(call myclass)", New NonCanonicalCtorCalls(CByte(1)).Log) + Assert.Equal("Base(lower)", New NonCanonicalCtorCalls("lower").Log) + Assert.Equal("Base(upper);Derived(Integer)", New NonCanonicalCtorCalls(1).Log) + Assert.Equal("Base(myclass);Derived(Long)", New NonCanonicalCtorCalls(1L).Log) + Assert.Equal("Base();Derived(Short)", New NonCanonicalCtorCalls(1S).Log) + Assert.Equal("Base(upper);Derived(Integer);Derived()", New NonCanonicalCtorCalls().Log) + Assert.Equal("Base(call);Derived(Double)", New NonCanonicalCtorCalls(1.0).Log) + Assert.Equal("Base(upper);Derived(Integer);Derived(Decimal)", New NonCanonicalCtorCalls(1D).Log) + Assert.Equal("Base(myclass);Derived(Long);Derived(Byte)", New NonCanonicalCtorCalls(CByte(1)).Log) End Sub End Class @@ -40,16 +40,16 @@ Module Program MyBase.new(value) End Sub Public Sub New(value As Integer) - MYBASE.NEW("me") - Log &= ";Derived(me)" + MYBASE.NEW("upper") + Log &= ";Derived(Integer)" End Sub Public Sub New(value As Long) myclass.New("myclass") - Log &= ";Derived(myclass)" + Log &= ";Derived(Long)" End Sub Public Sub New(value As Short) mybase.nEw - Log &= ";Derived(short)" + Log &= ";Derived(Short)" End Sub Public Sub New() Me.new(1) @@ -57,15 +57,15 @@ Module Program End Sub Public Sub New(value As Double) Call MyBase.new("call") - Log &= ";Derived(call)" + Log &= ";Derived(Double)" End Sub Public Sub New(value As Decimal) CALL ME.NEW(1) - Log &= ";Derived(call me)" + Log &= ";Derived(Decimal)" End Sub Public Sub New(value As Byte) call myclass.New(1L) - Log &= ";Derived(call myclass)" + Log &= ";Derived(Byte)" End Sub End Class