diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 74a905a13..e942dee50 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -949,24 +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 ctor = (node.Statements.FirstOrDefault() as VBSyntax.ExpressionStatementSyntax)?.Expression as VBSyntax.InvocationExpressionSyntax; - var ctorExpression = ctor?.Expression as VBSyntax.MemberAccessExpressionSyntax; - var ctorArgs = await (ctor?.ArgumentList).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()); @@ -980,6 +968,34 @@ 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)". + /// + /// 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, + VBSyntax.CallStatementSyntax callStatement => callStatement.Invocation, + _ => 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; + } + + return memberAccess.Expression switch { + VBSyntax.MyBaseExpressionSyntax => (CS.SyntaxKind.BaseConstructorInitializer, invocation?.ArgumentList), + VBSyntax.MeExpressionSyntax or VBSyntax.MyClassExpressionSyntax => (CS.SyntaxKind.ThisConstructorInitializer, invocation?.ArgumentList), + _ => null + }; + } + public override async Task VisitDeclareStatement(VBSyntax.DeclareStatementSyntax node) { var importAttributes = new List(); diff --git a/CodeConverter/Util/SyntaxTokenExtensions.cs b/CodeConverter/Util/SyntaxTokenExtensions.cs index 6fcefdb2f..5082e7c40 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); + // 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); } 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..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; @@ -220,4 +221,373 @@ public StaticLocalConvertedToField(int x) } }"); } + + [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 + Console.WriteLine(value) + End Sub +End Class", @"using System; + +public partial class Base +{ + public Base() + { + } + public Base(string value) + { + } +} + +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) + { + } +}"); + } + + [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()) + { + } +}"); + } + + [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) + { + } +}"); + } + + [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() + { + } +}"); + } + + [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 diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb index b51d5ed64..fc19e9fac 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/InheritanceTests.vb @@ -10,6 +10,63 @@ Module Program Dim x As A = New B Assert.True(x.TestMethod()) End Sub + + + Public Sub ConstructorInitializersWithNonCanonicalCasing() + 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 + + 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("upper") + Log &= ";Derived(Integer)" + End Sub + Public Sub New(value As Long) + myclass.New("myclass") + Log &= ";Derived(Long)" + 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 + Public Sub New(value As Double) + Call MyBase.new("call") + Log &= ";Derived(Double)" + End Sub + Public Sub New(value As Decimal) + CALL ME.NEW(1) + Log &= ";Derived(Decimal)" + End Sub + Public Sub New(value As Byte) + call myclass.New(1L) + Log &= ";Derived(Byte)" + End Sub End Class Public Interface IA