From 086a567ed52e406c487f663c7902060d4567e6c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:18:31 +0000 Subject: [PATCH] Convert omitted VB parameter lists to empty C# parameter lists consistently VB allows parentheses to be omitted for parameterless delegates and RaiseEvent accessors, leaving a null ParameterList. Delegates crashed with a null parameterList and RaiseEvent accessors silently lost their On method. Add AcceptParameterListAsync and use it everywhere a VB parameter list is converted (methods, delegates, accessors, Declare, operators, constructors, lambdas) instead of ad-hoc null fallbacks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../AccessorDeclarationNodeConverter.cs | 4 +- .../CSharp/DeclarationNodeVisitor.cs | 13 +++-- CodeConverter/CSharp/LambdaConverter.cs | 4 +- .../CSharp/SyntaxNodeVisitorExtensions.cs | 7 +++ Tests/CSharp/MemberTests/MemberTests.cs | 35 ++++++++++++ Tests/CSharp/NamespaceLevelTests.cs | 19 +++++++ Tests/CSharp/SpecialConversionTests.cs | 53 +++++++++++++++++++ 7 files changed, 124 insertions(+), 11 deletions(-) diff --git a/CodeConverter/CSharp/AccessorDeclarationNodeConverter.cs b/CodeConverter/CSharp/AccessorDeclarationNodeConverter.cs index 10f1c0f20..0ccaa2ffe 100644 --- a/CodeConverter/CSharp/AccessorDeclarationNodeConverter.cs +++ b/CodeConverter/CSharp/AccessorDeclarationNodeConverter.cs @@ -177,7 +177,7 @@ CSSyntax.MethodDeclarationSyntax WithMergedModifiers(CSSyntax.MethodDeclarationS async Task CreateMethodDeclarationSyntaxAsync(VBSyntax.ParameterListSyntax containingPropParameterList, string methodId, bool voidReturn) { - var parameterListSyntax = await containingPropParameterList.AcceptAsync(CommonConversions.TriviaConvertingExpressionVisitor); + var parameterListSyntax = await containingPropParameterList.AcceptParameterListAsync(CommonConversions.TriviaConvertingExpressionVisitor); var methodModifiers = CS.SyntaxFactory.TokenList(modifiers.Where(m => !m.IsCsVisibility(false, false))); CSSyntax.MethodDeclarationSyntax methodDeclarationSyntax = CS.SyntaxFactory.MethodDeclaration(attributes, methodModifiers, voidReturn ? CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword)) : rawType, @@ -274,7 +274,7 @@ public async Task VisitAccessorBlockAsync(VBSyntax.AccessorBlo async Task CreateMethodDeclarationSyntax(VBSyntax.ParameterListSyntax containingPropParameterList, bool voidReturn) { - var parameterListSyntax = await containingPropParameterList.AcceptAsync(CommonConversions.TriviaConvertingExpressionVisitor, sourceMap); + var parameterListSyntax = await containingPropParameterList.AcceptParameterListAsync(CommonConversions.TriviaConvertingExpressionVisitor, sourceMap); CSSyntax.MethodDeclarationSyntax methodDeclarationSyntax = CS.SyntaxFactory.MethodDeclaration(attributes, modifiers, voidReturn ? CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword)) : returnType, diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 74a905a13..9ce434337 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -496,7 +496,7 @@ public override async Task VisitDelegateStatement(VBSyntax.Del CS.SyntaxFactory.List(attributes), CommonConversions.ConvertModifiers(node, node.Modifiers), returnType, CommonConversions.ConvertIdentifier(node.Identifier), typeParameters, - await node.ParameterList.AcceptAsync(_triviaConvertingExpressionVisitor), + await node.ParameterList.AcceptParameterListAsync(_triviaConvertingExpressionVisitor), constraints ); } @@ -795,7 +795,7 @@ public override async Task VisitMethodStatement(VBSyntax.Metho await (node.AsClause?.Type).AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword))); var directlyConvertedCsIdentifier = CommonConversions.CsEscapedIdentifier(node.Identifier.Value as string); - var parameterList = await node.ParameterList.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ParameterList(); + var parameterList = await node.ParameterList.AcceptParameterListAsync(_triviaConvertingExpressionVisitor); var additionalDeclarations = new List(); var hasExplicitInterfaceImplementation = declaredSymbol.IsNonPublicInterfaceImplementation() || declaredSymbol.IsRenamedInterfaceMember(directlyConvertedCsIdentifier, declaredSymbol.ExplicitInterfaceImplementations); @@ -892,7 +892,7 @@ public override async Task VisitEventStatement(VBSyntax.EventS CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword)), delegateName, null, - await node.ParameterList.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ParameterList(), + await node.ParameterList.AcceptParameterListAsync(_triviaConvertingExpressionVisitor), CS.SyntaxFactory.List() ); @@ -927,7 +927,7 @@ public override async Task VisitOperatorStatement(VBSyntax.Ope var attributes = await node.AttributeLists.SelectManyAsync(CommonConversions.ConvertAttributeAsync); var attributeList = CS.SyntaxFactory.List(attributes); var returnType = await (node.AsClause?.Type).AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword)); - var parameterList = await node.ParameterList.AcceptAsync(_triviaConvertingExpressionVisitor); + var parameterList = await node.ParameterList.AcceptParameterListAsync(_triviaConvertingExpressionVisitor); var methodBodyVisitor = await ConvertMethodBodyStatementsAsync(node, containingBlock.Statements); var body = CS.SyntaxFactory.Block(methodBodyVisitor); var modifiers = CommonConversions.ConvertModifiers(node, node.Modifiers, node.GetMemberContext()); @@ -974,7 +974,7 @@ public override async Task VisitConstructorBlock(VBSyntax.Cons CS.SyntaxFactory.List(attributes), modifiers, CommonConversions.ConvertIdentifier(node.GetAncestor().BlockStatement.Identifier).WithoutSourceMapping(), //TODO Use semantic model for this name - await block.ParameterList.AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.ParameterList(), + await block.ParameterList.AcceptParameterListAsync(_triviaConvertingExpressionVisitor), ctorCall, CS.SyntaxFactory.Block(convertedBodyStatements) ); @@ -1010,8 +1010,7 @@ public override async Task VisitDeclareStatement(VBSyntax.Decl modifiers = modifiers.Add(CS.SyntaxFactory.Token(CS.SyntaxKind.ExternKeyword)); var returnType = await (node.AsClause?.Type).AcceptAsync(_triviaConvertingExpressionVisitor) ?? CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.VoidKeyword)); - var parameterListSyntax = await (node.ParameterList).AcceptAsync(_triviaConvertingExpressionVisitor) ?? - CS.SyntaxFactory.ParameterList(); + var parameterListSyntax = await (node.ParameterList).AcceptParameterListAsync(_triviaConvertingExpressionVisitor); return CS.SyntaxFactory.MethodDeclaration(attributeLists, modifiers, returnType, null, CommonConversions.ConvertIdentifier(node.Identifier), null, parameterListSyntax, CS.SyntaxFactory.List(), null, null).WithSemicolonToken(SemicolonToken); diff --git a/CodeConverter/CSharp/LambdaConverter.cs b/CodeConverter/CSharp/LambdaConverter.cs index 12f520017..bffb24015 100644 --- a/CodeConverter/CSharp/LambdaConverter.cs +++ b/CodeConverter/CSharp/LambdaConverter.cs @@ -40,7 +40,7 @@ public async Task ConvertMultiLineLambdaAsync(VBSyntax.MultiLi async Task ConvertInnerAsync() { var body = await ConvertMethodBodyStatementsAsync(node, node.Statements); - var param = await node.SubOrFunctionHeader.ParameterList.AcceptAsync(CommonConversions.TriviaConvertingExpressionVisitor); + var param = await node.SubOrFunctionHeader.ParameterList.AcceptParameterListAsync(CommonConversions.TriviaConvertingExpressionVisitor); return await ConvertAsync(node, param, body.ToList()); } } @@ -66,7 +66,7 @@ async Task ConvertInnerAsync() convertedStatements = new[] { SyntaxFactory.ExpressionStatement(csNode) }; } - var param = await node.SubOrFunctionHeader.ParameterList.AcceptAsync(CommonConversions.TriviaConvertingExpressionVisitor); + var param = await node.SubOrFunctionHeader.ParameterList.AcceptParameterListAsync(CommonConversions.TriviaConvertingExpressionVisitor); return await ConvertAsync(node, param, convertedStatements); } } diff --git a/CodeConverter/CSharp/SyntaxNodeVisitorExtensions.cs b/CodeConverter/CSharp/SyntaxNodeVisitorExtensions.cs index ee9a1c641..772bf361a 100644 --- a/CodeConverter/CSharp/SyntaxNodeVisitorExtensions.cs +++ b/CodeConverter/CSharp/SyntaxNodeVisitorExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.VisualBasic; namespace ICSharpCode.CodeConverter.CSharp; @@ -9,6 +10,12 @@ internal static class SyntaxNodeVisitorExtensions /// For TOut, specify the most general type acceptable by the calling code (often ExpressionSyntax), this allows error information to be attached to a dummy return node. public static async Task AcceptAsync(this VisualBasicSyntaxNode node, CommentConvertingVisitorWrapper visitorWrapper, SourceTriviaMapKind sourceTriviaMap = SourceTriviaMapKind.All) where TOut : CSharpSyntaxNode => node == null ? null : await visitorWrapper.AcceptAsync(node, sourceTriviaMap); + /// + /// VB allows the parentheses to be omitted when there are no parameters (e.g. Delegate Sub Callback, RaiseEvent accessors, Sub Foo), leaving a null parameter list. + /// C# always requires one, so every place that converts a VB parameter list should go via this method so they all agree on an empty list. + /// + public static async Task AcceptParameterListAsync(this VBSyntax.ParameterListSyntax parameterList, CommentConvertingVisitorWrapper visitorWrapper, SourceTriviaMapKind sourceTriviaMap = SourceTriviaMapKind.All) => + await parameterList.AcceptAsync(visitorWrapper, sourceTriviaMap) ?? Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParameterList(); public static async Task> AcceptSeparatedListAsync(this SeparatedSyntaxList nodes, CommentConvertingVisitorWrapper visitorWrapper, SourceTriviaMapKind sourceTriviaMap = SourceTriviaMapKind.All) where TIn : VisualBasicSyntaxNode where TOut : CSharpSyntaxNode => await visitorWrapper.AcceptAsync(nodes, sourceTriviaMap); } \ No newline at end of file diff --git a/Tests/CSharp/MemberTests/MemberTests.cs b/Tests/CSharp/MemberTests/MemberTests.cs index 57dca8153..f1a5366ac 100644 --- a/Tests/CSharp/MemberTests/MemberTests.cs +++ b/Tests/CSharp/MemberTests/MemberTests.cs @@ -1645,6 +1645,41 @@ internal string Join(string s = null) }"); } + [Fact] + public async Task ParameterlessMembersWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Class TestClass + Declare Sub ExternSub Lib ""lib"" + Declare Function ExternFunc Lib ""lib"" As Integer + Sub New + End Sub + Sub Foo + End Sub + Function Bar As Integer + Return 1 + End Function +End Class", @"using System.Runtime.InteropServices; + +internal partial class TestClass +{ + [DllImport(""lib"")] + public static extern void ExternSub(); + [DllImport(""lib"")] + public static extern int ExternFunc(); + public TestClass() + { + } + public void Foo() + { + } + public int Bar() + { + return 1; + } +}"); + } + [Fact] public async Task TestMultipleCharDefaultValuesForStringParametersAsync() { diff --git a/Tests/CSharp/NamespaceLevelTests.cs b/Tests/CSharp/NamespaceLevelTests.cs index 81a8c20b6..983da4f4a 100644 --- a/Tests/CSharp/NamespaceLevelTests.cs +++ b/Tests/CSharp/NamespaceLevelTests.cs @@ -397,6 +397,25 @@ await TestConversionVisualBasicToCSharpAsync( @"public delegate void Test(ref int x);"); } + [Fact] + public async Task TestParameterlessDelegateWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Delegate Sub Callback", + @"public delegate void Callback();"); + await TestConversionVisualBasicToCSharpAsync( + @"Public Delegate Function Provider As Integer", + @"public delegate int Provider();"); + } + + [Fact] + public async Task TestGenericParameterlessDelegateWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Delegate Function Operation(Of T) As T", + @"public delegate T Operation();"); + } + [Fact] public async Task TestGenericDelegate771Async() { diff --git a/Tests/CSharp/SpecialConversionTests.cs b/Tests/CSharp/SpecialConversionTests.cs index 9b0713ca1..2631b3106 100644 --- a/Tests/CSharp/SpecialConversionTests.cs +++ b/Tests/CSharp/SpecialConversionTests.cs @@ -80,6 +80,59 @@ public void RaiseCustomEvent() }"); } + [Fact] + public async Task TestCustomEventWithParameterlessRaiseEventWithoutParenthesesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Class TestClass + Public Delegate Sub Callback() + Private Event backingField As Callback + + Public Custom Event MyEvent As Callback + AddHandler(ByVal value As Callback) + AddHandler Me.backingField, value + End AddHandler + RemoveHandler(ByVal value As Callback) + RemoveHandler Me.backingField, value + End RemoveHandler + RaiseEvent + Console.WriteLine(""Event Raised"") + End RaiseEvent + End Event ' RaiseEvent moves outside this block + + Public Sub RaiseCustomEvent() + RaiseEvent MyEvent() + End Sub +End Class", @"using System; + +internal partial class TestClass +{ + public delegate void Callback(); + private event Callback backingField; + + public event Callback MyEvent + { + add + { + backingField += value; + } + remove + { + backingField -= value; + } + } // RaiseEvent moves outside this block + void OnMyEvent() + { + Console.WriteLine(""Event Raised""); + } + + public void RaiseCustomEvent() + { + OnMyEvent(); + } +}"); + } + [Fact] public async Task TestFullWidthCharacterCustomEventAsync() {