From e5a589340d90c9630614f2337ca239e30ec9dc18 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:23:08 +0000 Subject: [PATCH 1/9] WIP: Add failing repro test for ValueTuple constructor conversion Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- Tests/CSharp/ExpressionTests/ExpressionTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index ef68b4387..bed659fbf 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2385,6 +2385,19 @@ await TestConversionVisualBasicToCSharpAsync( }"); } + [Fact] + public async Task ValueTupleConstructorBecomesTupleLiteralAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Function Foo() As ValueTuple(Of Double, Double) + Return New ValueTuple(Of Double, Double)(1, 2) +End Function", + @"public (double, double) Foo() +{ + return (1d, 2d); +}"); + } + [Fact] public async Task UseEventBackingFieldAsync() { From 25feff769013711ad75ec52a586ef24ed096e84c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 15:24:14 +0000 Subject: [PATCH 2/9] Name tuple types by their generic ValueTuple type after new C# reports CS8181 for `new (int, int)(...)`, so an object creation of a tuple type now uses `new ValueTuple(...)`. This makes every ValueTuple construction compile; simpler forms follow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/CommonConversions.cs | 10 +++++++++ CodeConverter/CSharp/ExpressionNodeVisitor.cs | 4 ++-- .../CSharp/ExpressionTests/ExpressionTests.cs | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs index 3f37f1985..493f6b8a2 100644 --- a/CodeConverter/CSharp/CommonConversions.cs +++ b/CodeConverter/CSharp/CommonConversions.cs @@ -219,6 +219,16 @@ public TypeSyntax GetTypeSyntax(ITypeSymbol typeSymbol, bool useImplicitType = f return WithDeclarationNameCasing(syntax, typeSymbol); } + /// + /// C# forbids tuple type syntax after new (CS8181), so a tuple type is named by its generic ValueTuple type there. + /// + public TypeSyntax GetValueTupleTypeSyntax(INamedTypeSymbol tupleType) + { + var typeArguments = (tupleType.TupleUnderlyingType ?? tupleType).TypeArguments.Select(t => GetTypeSyntax(t)); + var valueTupleName = SyntaxFactory.GenericName(SyntaxFactory.Identifier(nameof(ValueTuple)), SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArguments))); + return SyntaxFactory.QualifiedName(GetFullyQualifiedNameSyntax(tupleType.ContainingNamespace), valueTupleName); + } + /// /// Semantic model merges the symbols, but the compiled form retains multiple namespaces, which (when referenced from C#) need to keep the correct casing. /// diff --git a/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index a05a9739d..551188cc7 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -281,9 +281,9 @@ public override async Task VisitAnonymousObjectCreationExpress public override async Task VisitObjectCreationExpression(VBasic.Syntax.ObjectCreationExpressionSyntax node) { - + var createdType = _semanticModel.GetTypeInfo(node).Type; var objectCreationExpressionSyntax = CS.SyntaxFactory.ObjectCreationExpression( - await node.Type.AcceptAsync(TriviaConvertingExpressionVisitor), + createdType is INamedTypeSymbol {IsTupleType: true} tupleType ? CommonConversions.GetValueTupleTypeSyntax(tupleType) : await node.Type.AcceptAsync(TriviaConvertingExpressionVisitor), // VB can omit empty arg lists: await _argumentConverter.ConvertArgumentListOrEmptyAsync(node, node.ArgumentList), null diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index bed659fbf..4bf1de07b 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2398,6 +2398,27 @@ Return New ValueTuple(Of Double, Double)(1, 2) }"); } + [Fact] + public async Task ValueTupleConstructorKeepsConstructorWhenTupleLiteralWouldChangeMeaningAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Sub Foo(s As String, x As Integer) + Dim boxed = New ValueTuple(Of Object, Object)(s, 1) + Dim nullable = New ValueTuple(Of Integer?, Integer)(x, 1) + Dim named = New ValueTuple(Of Integer, String)(item2:=s, item1:=x) + Dim initialized = New ValueTuple(Of Integer, Integer) With {.Item1 = x} + Dim oneElement = New ValueTuple(Of Integer)(x) +End Sub", + @"public void Foo(string s, int x) +{ + var boxed = new ValueTuple(s, 1); + var nullable = new ValueTuple(x, 1); + var named = new ValueTuple(item2: s, item1: x); + var initialized = new ValueTuple() { Item1 = x }; + var oneElement = new ValueTuple(x); +}"); + } + [Fact] public async Task UseEventBackingFieldAsync() { From f7c722dc00eba563faca48dc3607f1bcfb19ae9d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 15:39:24 +0000 Subject: [PATCH 3/9] Convert New ValueTuple(Of ...)(args) to a tuple literal Fixes icsharpcode/CodeConverter#1027: `New ValueTuple(Of Double, Double)(1, 2)` now becomes `(1d, 2d)`. A tuple literal only takes its element types from a target type when it has one (not when assigned to var), so it's used only when every converted argument already has its element's type. Otherwise (boxing, int to int?, lambdas), and for 1-tuples, named arguments, initializers and expression trees (CS8143), the creation stays `new ValueTuple<...>(...)`. A TRest created inline is flattened, as C# lays out longer literals. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/ExpressionNodeVisitor.cs | 3 + .../CSharp/TypeConversionAnalyzer.cs | 17 +++ .../CSharp/ValueTupleCreationConverter.cs | 59 ++++++++++ .../CSharp/ExpressionTests/ExpressionTests.cs | 60 ++++++++++ .../VBToCS/ValueTupleCreationTests.vb | 107 ++++++++++++++++++ 5 files changed, 246 insertions(+) create mode 100644 CodeConverter/CSharp/ValueTupleCreationConverter.cs create mode 100644 Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb diff --git a/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index 551188cc7..ae0b2dfba 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -32,6 +32,7 @@ internal partial class ExpressionNodeVisitor : VBasic.VisualBasicSyntaxVisitor VisitAnonymousObjectCreationExpress public override async Task VisitObjectCreationExpression(VBasic.Syntax.ObjectCreationExpressionSyntax node) { + if (await _valueTupleCreationConverter.ConvertToTupleLiteralOrNullAsync(node) is { } tupleLiteral) return tupleLiteral; var createdType = _semanticModel.GetTypeInfo(node).Type; var objectCreationExpressionSyntax = CS.SyntaxFactory.ObjectCreationExpression( createdType is INamedTypeSymbol {IsTupleType: true} tupleType ? CommonConversions.GetValueTupleTypeSyntax(tupleType) : await node.Type.AcceptAsync(TriviaConvertingExpressionVisitor), diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index d1c3d3d24..35b4aa399 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -354,6 +354,23 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, return false; } + /// + /// Whether the expression produces + /// has the converted type as its natural C# type, so keeps that type where nothing target-types it (e.g. an element of a tuple literal assigned to var). + /// Conversions C# does implicitly (e.g. boxing, int to int?, lambda to delegate) are left to the target type, so don't. + /// + public bool KeepsConvertedTypeWithoutTargetType(VBSyntax.ExpressionSyntax vbNode) + { + var (vbType, vbConvertedType) = GetTypeInfo(vbNode); + if (vbConvertedType is null) return false; + var sourceIsConvertedType = SymbolEqualityComparer.Default.Equals(vbType, vbConvertedType); + return AnalyzeConversion(vbNode) switch { + TypeConversionKind.Unknown or TypeConversionKind.Identity or TypeConversionKind.DelegateConstructor => sourceIsConvertedType, + TypeConversionKind.LiteralSuffix => sourceIsConvertedType || IsExactTypeNumericLiteral(vbNode, vbConvertedType), + _ => true + }; + } + private static bool IsExactTypeNumericLiteral(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbConvertedType) => vbNode is VBSyntax.LiteralExpressionSyntax literal && LiteralConversions.ConvertLiteralNumericValueOrNull(literal.Token.Value, vbConvertedType) is {}; diff --git a/CodeConverter/CSharp/ValueTupleCreationConverter.cs b/CodeConverter/CSharp/ValueTupleCreationConverter.cs new file mode 100644 index 000000000..96ec2eda5 --- /dev/null +++ b/CodeConverter/CSharp/ValueTupleCreationConverter.cs @@ -0,0 +1,59 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ICSharpCode.CodeConverter.CSharp; + +/// +/// Converts New ValueTuple(Of T1, T2)(a, b) to the tuple literal (a, b), which creates the same value. +/// C# can't write new (T1, T2)(a, b) (CS8181), so anything this declines is created with new ValueTuple<T1, T2>(a, b). +/// +/// +/// A tuple literal's element types come from a target type if it has one, and from its elements otherwise (e.g. assigned to var). +/// Only a literal whose elements already have the VB type arguments keeps the same type in both cases. +/// It's declined for: a 1-tuple (no literal syntax), named arguments, object initializers, +/// and expression trees, which can't contain a tuple literal (CS8143). +/// +internal class ValueTupleCreationConverter +{ + /// The position of TRest in ValueTuple(Of T1, ..., T7, TRest), which a longer tuple literal nests its remaining elements in. + private const int RestIndex = 7; + + private readonly SemanticModel _semanticModel; + private readonly CommonConversions _commonConversions; + + public ValueTupleCreationConverter(SemanticModel semanticModel, CommonConversions commonConversions) + { + _semanticModel = semanticModel; + _commonConversions = commonConversions; + } + + public async Task ConvertToTupleLiteralOrNullAsync(VBSyntax.ObjectCreationExpressionSyntax node) + { + if (_commonConversions.TriviaConvertingExpressionVisitor.IsWithinExpressionTree) return null; + var elements = GetTupleElementsOrNull(node); + if (elements is not {Count: > 1} || !elements.All(e => _commonConversions.TypeConversionAnalyzer.KeepsConvertedTypeWithoutTargetType(e.Expression))) return null; + + var arguments = await elements.SelectAsync(async e => await e.AcceptAsync(_commonConversions.TriviaConvertingExpressionVisitor)); + return SyntaxFactory.TupleExpression(SyntaxFactory.SeparatedList(arguments)); + } + + /// + /// The arguments in element order, flattening a TRest created inline, since that's how a tuple literal with more than 7 elements is laid out. + /// + private List GetTupleElementsOrNull(VBSyntax.ObjectCreationExpressionSyntax node) + { + if (node.Initializer != null || _semanticModel.GetTypeInfo(node).Type is not {IsTupleType: true}) return null; + if (node.ArgumentList?.Arguments.All(a => a is VBSyntax.SimpleArgumentSyntax {IsNamed: false}) != true) return null; + + var elements = node.ArgumentList.Arguments.Cast().ToList(); + if (elements.Count <= RestIndex) return elements; + + var restElements = elements[RestIndex].Expression is VBSyntax.ObjectCreationExpressionSyntax restCreation && HasNoConversion(restCreation) + ? GetTupleElementsOrNull(restCreation) + : null; + return restElements is null ? null : elements.Take(RestIndex).Concat(restElements).ToList(); + } + + private bool HasNoConversion(VBSyntax.ExpressionSyntax expression) => + _semanticModel.GetTypeInfo(expression) is var typeInfo && SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType); +} diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index 4bf1de07b..08fba968f 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2398,6 +2398,46 @@ Return New ValueTuple(Of Double, Double)(1, 2) }"); } + [Fact] + public async Task ValueTupleConstructorBecomesTupleLiteralWithElementsOfTheTypeArgumentsAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Sub Foo(x As Integer, s As Short) + Dim widened = New ValueTuple(Of Long, Short)(x, s) + Dim literals = New ValueTuple(Of Double, Long, Byte)(1, 2, 3) + Dim shortArithmetic = New ValueTuple(Of Short, Integer)(s * s, s * s) + Dim stringAndChar = New ValueTuple(Of String, Char)(""a"", ""b""c) + Dim nothings = New ValueTuple(Of String, Integer)(Nothing, Nothing) +End Sub", + @"public void Foo(int x, short s) +{ + var widened = ((long)x, s); + var literals = (1d, 2L, (byte)3); + var shortArithmetic = ((short)(s * s), s * s); + var stringAndChar = (""a"", 'b'); + var nothings = ((string)null, default(int)); +}"); + } + + [Fact] + public async Task ValueTupleConstructorWithInlineRestBecomesFlatTupleLiteralAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Sub Foo(rest As ValueTuple(Of Integer)) + Dim eight = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) + Dim nine = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer, Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer, Long)(8, 9)) + Dim restVariable = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, rest) + Dim restWidened = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) +End Sub", + @"public void Foo(ValueTuple rest) +{ + var eight = (1, 2, 3, 4, 5, 6, 7, 8); + var nine = (1, 2, 3, 4, 5, 6, 7, 8, 9L); + var restVariable = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, rest); + var restWidened = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, new ValueTuple(8)); +}"); + } + [Fact] public async Task ValueTupleConstructorKeepsConstructorWhenTupleLiteralWouldChangeMeaningAsync() { @@ -2408,6 +2448,7 @@ await TestConversionVisualBasicToCSharpAsync( Dim named = New ValueTuple(Of Integer, String)(item2:=s, item1:=x) Dim initialized = New ValueTuple(Of Integer, Integer) With {.Item1 = x} Dim oneElement = New ValueTuple(Of Integer)(x) + Dim lambda = New ValueTuple(Of Func(Of Integer), Integer)(Function() x, 2) End Sub", @"public void Foo(string s, int x) { @@ -2416,6 +2457,25 @@ await TestConversionVisualBasicToCSharpAsync( var named = new ValueTuple(item2: s, item1: x); var initialized = new ValueTuple() { Item1 = x }; var oneElement = new ValueTuple(x); + var lambda = new ValueTuple, int>(() => x, 2); +}"); + } + + [Fact] + public async Task ValueTupleConstructorInExpressionTreeKeepsConstructorAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Imports System.Linq.Expressions + +Class C + Private ReadOnly _create As Expression(Of Func(Of Integer, (Integer, Integer))) = Function(x) New ValueTuple(Of Integer, Integer)(x, 2) +End Class", + @"using System; +using System.Linq.Expressions; + +internal partial class C +{ + private readonly Expression> _create = x => new ValueTuple(x, 2); }"); } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb new file mode 100644 index 000000000..5d645f7e6 --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb @@ -0,0 +1,107 @@ +Imports System +Imports System.Collections.Generic +Imports System.Linq.Expressions +Imports Xunit + +Public Class ValueTupleCreationTests + Private ReadOnly _log As New List(Of String) + + Private Function Logged(Of T)(name As String, value As T) As T + _log.Add(name) + Return value + End Function + + + Sub ElementsKeepTheTypeArgumentsWithoutATargetType() + Dim x As Integer = 1 + Dim s As Short = 2 + Dim widened = New ValueTuple(Of Long, Short)(x, s) + Assert.Equal(GetType(ValueTuple(Of Long, Short)), widened.GetType()) + Assert.Equal((1L, CShort(2)), widened) + + Dim literals = New ValueTuple(Of Double, Long, Byte, Decimal, Single, Short)(1, 2, 3, 4, 5, 6) + Assert.Equal(GetType(ValueTuple(Of Double, Long, Byte, Decimal, Single, Short)), literals.GetType()) + + Dim shortArithmetic = New ValueTuple(Of Short, Integer)(s * s, s * s) + Assert.Equal(GetType(ValueTuple(Of Short, Integer)), shortArithmetic.GetType()) + Assert.Equal((CShort(4), 4), shortArithmetic) + + Dim stringAndChar = New ValueTuple(Of String, Char)("a", "b"c) + Assert.Equal(("a", "b"c), stringAndChar) + End Sub + + + Sub ElementsNeedingATargetTypeKeepTheTypeArguments() + Dim x As Integer = 1 + Dim boxed = New ValueTuple(Of Object, Object)("a", x) + Assert.Equal(GetType(ValueTuple(Of Object, Object)), boxed.GetType()) + + Dim nullable = New ValueTuple(Of Integer?, Integer?)(x, 2) + Assert.Equal(GetType(ValueTuple(Of Integer?, Integer?)), nullable.GetType()) + + Dim nothings = New ValueTuple(Of String, Integer)(Nothing, Nothing) + Assert.Equal(GetType(ValueTuple(Of String, Integer)), nothings.GetType()) + Assert.Equal((CStr(Nothing), 0), nothings) + + Dim lambda = New ValueTuple(Of Func(Of Integer), Integer)(Function() 3, 4) + Assert.Equal(3, lambda.Item1()) + End Sub + + + Sub TargetTypedCreationKeepsValues() + Dim declared As (A As Double, B As Long) = New ValueTuple(Of Double, Long)(1, 2) + Assert.Equal(1.0, declared.A) + Assert.Equal(2L, declared.B) + Assert.Equal((1.0, 2L), ReturnTuple()) + End Sub + + Private Function ReturnTuple() As ValueTuple(Of Double, Long) + Return New ValueTuple(Of Double, Long)(1, 2) + End Function + + + Sub ElementsAreEvaluatedInOrder() + Dim positional = New ValueTuple(Of Integer, Integer)(Logged("first", 1), Logged("second", 2)) + Assert.Equal({"first", "second"}, _log.ToArray()) + Assert.Equal((1, 2), positional) + End Sub + + + Sub NamedArgumentsKeepValues() + Dim named = New ValueTuple(Of Integer, Integer)(item2:=2, item1:=1) + Assert.Equal((1, 2), named) + End Sub + + + Sub OneElementAndInitializedTuples() + Dim one = New ValueTuple(Of Integer)(5) + Assert.Equal(5, one.Item1) + + Dim initialized = New ValueTuple(Of Integer, String) With {.Item2 = "b"} + Assert.Equal((0, "b"), initialized) + End Sub + + + Sub LongTuplesKeepAllElements() + Dim eight = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) + Assert.Equal(GetType(ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))), eight.GetType()) + Assert.Equal(8, eight.Rest.Item1) + + Dim nine = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer, Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer, Long)(8, 9)) + Assert.Equal(GetType(ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer, Long))), nine.GetType()) + Assert.Equal(9L, nine.Rest.Item2) + + Dim rest = New ValueTuple(Of Integer)(8) + Dim restVariable = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, rest) + Assert.Equal(eight, restVariable) + + Dim restWidened = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) + Assert.Equal(GetType(ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))), restWidened.GetType()) + End Sub + + + Sub ExpressionTreeCreatesTuple() + Dim create As Expression(Of Func(Of Integer, (Integer, Integer))) = Function(x) New ValueTuple(Of Integer, Integer)(x, 2) + Assert.Equal((1, 2), create.Compile()(1)) + End Sub +End Class From a0658632ba5c7b84ecb590bf5f0922d9767f2fca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 15:56:33 +0000 Subject: [PATCH 4/9] Convert New ValueTuple(Of ...)() without arguments to default A struct's parameterless creation is its default value, and `default((int, string))` names the tuple type without constructing it, so it also simplifies to `default` where the type is inferred. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/ExpressionNodeVisitor.cs | 2 +- .../CSharp/ValueTupleCreationConverter.cs | 8 +++++--- .../CSharp/ExpressionTests/ExpressionTests.cs | 19 +++++++++++++++++++ .../VBToCS/ValueTupleCreationTests.vb | 10 ++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index ae0b2dfba..0ce945f73 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -283,7 +283,7 @@ public override async Task VisitAnonymousObjectCreationExpress public override async Task VisitObjectCreationExpression(VBasic.Syntax.ObjectCreationExpressionSyntax node) { - if (await _valueTupleCreationConverter.ConvertToTupleLiteralOrNullAsync(node) is { } tupleLiteral) return tupleLiteral; + if (await _valueTupleCreationConverter.ConvertOrNullAsync(node) is { } tupleCreation) return tupleCreation; var createdType = _semanticModel.GetTypeInfo(node).Type; var objectCreationExpressionSyntax = CS.SyntaxFactory.ObjectCreationExpression( createdType is INamedTypeSymbol {IsTupleType: true} tupleType ? CommonConversions.GetValueTupleTypeSyntax(tupleType) : await node.Type.AcceptAsync(TriviaConvertingExpressionVisitor), diff --git a/CodeConverter/CSharp/ValueTupleCreationConverter.cs b/CodeConverter/CSharp/ValueTupleCreationConverter.cs index 96ec2eda5..b4ba592af 100644 --- a/CodeConverter/CSharp/ValueTupleCreationConverter.cs +++ b/CodeConverter/CSharp/ValueTupleCreationConverter.cs @@ -4,8 +4,8 @@ namespace ICSharpCode.CodeConverter.CSharp; /// -/// Converts New ValueTuple(Of T1, T2)(a, b) to the tuple literal (a, b), which creates the same value. -/// C# can't write new (T1, T2)(a, b) (CS8181), so anything this declines is created with new ValueTuple<T1, T2>(a, b). +/// Converts New ValueTuple(Of T1, T2)(a, b) to the tuple literal (a, b), and New ValueTuple(Of T1, T2)() to default((T1, T2)), +/// which create the same values. C# can't write new (T1, T2)(a, b) (CS8181), so anything this declines is created with new ValueTuple<T1, T2>(a, b). /// /// /// A tuple literal's element types come from a target type if it has one, and from its elements otherwise (e.g. assigned to var). @@ -27,8 +27,10 @@ public ValueTupleCreationConverter(SemanticModel semanticModel, CommonConversion _commonConversions = commonConversions; } - public async Task ConvertToTupleLiteralOrNullAsync(VBSyntax.ObjectCreationExpressionSyntax node) + public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationExpressionSyntax node) { + if (node.Initializer != null || _semanticModel.GetTypeInfo(node).Type is not {IsTupleType: true} tupleType) return null; + if (node.ArgumentList?.Arguments.Any() != true) return SyntaxFactory.DefaultExpression(_commonConversions.GetTypeSyntax(tupleType)); if (_commonConversions.TriviaConvertingExpressionVisitor.IsWithinExpressionTree) return null; var elements = GetTupleElementsOrNull(node); if (elements is not {Count: > 1} || !elements.All(e => _commonConversions.TypeConversionAnalyzer.KeepsConvertedTypeWithoutTargetType(e.Expression))) return null; diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index 08fba968f..ccafc0cc2 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2419,6 +2419,25 @@ await TestConversionVisualBasicToCSharpAsync( }"); } + [Fact] + public async Task ValueTupleConstructorWithoutArgumentsBecomesDefaultAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Function Foo() As (Integer, String) + Dim pair = New ValueTuple(Of Integer, String)() + Dim noParentheses = New ValueTuple(Of Integer, String) + Dim oneElement = New ValueTuple(Of Integer)() + Return New ValueTuple(Of Integer, String) +End Function", + @"public (int, string) Foo() +{ + var pair = default((int, string)); + var noParentheses = default((int, string)); + var oneElement = default(ValueTuple); + return default; +}"); + } + [Fact] public async Task ValueTupleConstructorWithInlineRestBecomesFlatTupleLiteralAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb index 5d645f7e6..782747515 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb @@ -81,6 +81,16 @@ Public Class ValueTupleCreationTests Assert.Equal((0, "b"), initialized) End Sub + + Sub CreationWithoutArgumentsIsDefault() + Dim pair = New ValueTuple(Of Integer, String)() + Assert.Equal(GetType(ValueTuple(Of Integer, String)), pair.GetType()) + Assert.Equal((0, CStr(Nothing)), pair) + Dim noParentheses = New ValueTuple(Of Integer, String) + Assert.Equal(pair, noParentheses) + Assert.Equal(0, New ValueTuple(Of Integer)().Item1) + End Sub + Sub LongTuplesKeepAllElements() Dim eight = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) From 473b2dadf5a1d3342395565af63982ed029077f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 15:57:43 +0000 Subject: [PATCH 5/9] Don't flatten a TRest created without arguments into a tuple literal `New ValueTuple(Of T1..T7, ValueTuple(Of T8))(..., New ValueTuple(Of T8)())` flattened to a 7-element literal, a different type. It now keeps the constructor, with the empty rest as `default`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/ValueTupleCreationConverter.cs | 3 ++- Tests/CSharp/ExpressionTests/ExpressionTests.cs | 2 ++ .../SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CodeConverter/CSharp/ValueTupleCreationConverter.cs b/CodeConverter/CSharp/ValueTupleCreationConverter.cs index b4ba592af..c21914913 100644 --- a/CodeConverter/CSharp/ValueTupleCreationConverter.cs +++ b/CodeConverter/CSharp/ValueTupleCreationConverter.cs @@ -41,6 +41,7 @@ public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationEx /// /// The arguments in element order, flattening a TRest created inline, since that's how a tuple literal with more than 7 elements is laid out. + /// A TRest created without arguments has no elements to flatten: the literal would lose the nested tuple. /// private List GetTupleElementsOrNull(VBSyntax.ObjectCreationExpressionSyntax node) { @@ -53,7 +54,7 @@ public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationEx var restElements = elements[RestIndex].Expression is VBSyntax.ObjectCreationExpressionSyntax restCreation && HasNoConversion(restCreation) ? GetTupleElementsOrNull(restCreation) : null; - return restElements is null ? null : elements.Take(RestIndex).Concat(restElements).ToList(); + return restElements is {Count: > 0} ? elements.Take(RestIndex).Concat(restElements).ToList() : null; } private bool HasNoConversion(VBSyntax.ExpressionSyntax expression) => diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index ccafc0cc2..a5e1d2f13 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2447,6 +2447,7 @@ await TestConversionVisualBasicToCSharpAsync( Dim nine = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer, Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer, Long)(8, 9)) Dim restVariable = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, rest) Dim restWidened = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) + Dim restDefault = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)()) End Sub", @"public void Foo(ValueTuple rest) { @@ -2454,6 +2455,7 @@ await TestConversionVisualBasicToCSharpAsync( var nine = (1, 2, 3, 4, 5, 6, 7, 8, 9L); var restVariable = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, rest); var restWidened = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, new ValueTuple(8)); + var restDefault = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, default); }"); } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb index 782747515..555748d62 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb @@ -107,6 +107,9 @@ Public Class ValueTupleCreationTests Dim restWidened = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) Assert.Equal(GetType(ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))), restWidened.GetType()) + + Dim restDefault = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)()) + Assert.Equal(GetType(ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))), restDefault.GetType()) End Sub From 86e2c9fdddf4299482b8eda18c389c338003a8d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:01:19 +0000 Subject: [PATCH 6/9] Test that a TRest created with an initializer isn't flattened Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- Tests/CSharp/ExpressionTests/ExpressionTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index a5e1d2f13..c7e714849 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2448,6 +2448,7 @@ await TestConversionVisualBasicToCSharpAsync( Dim restVariable = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, rest) Dim restWidened = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Long))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)(8)) Dim restDefault = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer)()) + Dim restInitialized = New ValueTuple(Of Integer, Integer, Integer, Integer, Integer, Integer, Integer, ValueTuple(Of Integer))(1, 2, 3, 4, 5, 6, 7, New ValueTuple(Of Integer) With {.Item1 = 8}) End Sub", @"public void Foo(ValueTuple rest) { @@ -2456,6 +2457,7 @@ await TestConversionVisualBasicToCSharpAsync( var restVariable = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, rest); var restWidened = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, new ValueTuple(8)); var restDefault = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, default); + var restInitialized = new ValueTuple>(1, 2, 3, 4, 5, 6, 7, new ValueTuple() { Item1 = 8 }); }"); } From 207efa15aa13b1cedd2bab21d90e8c5c85eedf93 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:01:19 +0000 Subject: [PATCH 7/9] Remove unreachable branches found by coverage A VB tuple type always has a TupleUnderlyingType, and a literal-suffix conversion never has identical source and converted types. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/CommonConversions.cs | 2 +- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs index 493f6b8a2..0999dbf7e 100644 --- a/CodeConverter/CSharp/CommonConversions.cs +++ b/CodeConverter/CSharp/CommonConversions.cs @@ -224,7 +224,7 @@ public TypeSyntax GetTypeSyntax(ITypeSymbol typeSymbol, bool useImplicitType = f /// public TypeSyntax GetValueTupleTypeSyntax(INamedTypeSymbol tupleType) { - var typeArguments = (tupleType.TupleUnderlyingType ?? tupleType).TypeArguments.Select(t => GetTypeSyntax(t)); + var typeArguments = tupleType.TupleUnderlyingType.TypeArguments.Select(t => GetTypeSyntax(t)); var valueTupleName = SyntaxFactory.GenericName(SyntaxFactory.Identifier(nameof(ValueTuple)), SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArguments))); return SyntaxFactory.QualifiedName(GetFullyQualifiedNameSyntax(tupleType.ContainingNamespace), valueTupleName); } diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 35b4aa399..a8e6dec15 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -366,7 +366,7 @@ public bool KeepsConvertedTypeWithoutTargetType(VBSyntax.ExpressionSyntax vbNode var sourceIsConvertedType = SymbolEqualityComparer.Default.Equals(vbType, vbConvertedType); return AnalyzeConversion(vbNode) switch { TypeConversionKind.Unknown or TypeConversionKind.Identity or TypeConversionKind.DelegateConstructor => sourceIsConvertedType, - TypeConversionKind.LiteralSuffix => sourceIsConvertedType || IsExactTypeNumericLiteral(vbNode, vbConvertedType), + TypeConversionKind.LiteralSuffix => IsExactTypeNumericLiteral(vbNode, vbConvertedType), _ => true }; } From a158f1ccbbbd650b98b5e26efe147b8520539b2d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:43:48 +0000 Subject: [PATCH 8/9] Extract the plain tuple creation check shared by both conversions Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/ValueTupleCreationConverter.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/ValueTupleCreationConverter.cs b/CodeConverter/CSharp/ValueTupleCreationConverter.cs index c21914913..24ca69b69 100644 --- a/CodeConverter/CSharp/ValueTupleCreationConverter.cs +++ b/CodeConverter/CSharp/ValueTupleCreationConverter.cs @@ -29,7 +29,7 @@ public ValueTupleCreationConverter(SemanticModel semanticModel, CommonConversion public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationExpressionSyntax node) { - if (node.Initializer != null || _semanticModel.GetTypeInfo(node).Type is not {IsTupleType: true} tupleType) return null; + if (GetPlainTupleCreationTypeOrNull(node) is not { } tupleType) return null; if (node.ArgumentList?.Arguments.Any() != true) return SyntaxFactory.DefaultExpression(_commonConversions.GetTypeSyntax(tupleType)); if (_commonConversions.TriviaConvertingExpressionVisitor.IsWithinExpressionTree) return null; var elements = GetTupleElementsOrNull(node); @@ -45,7 +45,7 @@ public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationEx /// private List GetTupleElementsOrNull(VBSyntax.ObjectCreationExpressionSyntax node) { - if (node.Initializer != null || _semanticModel.GetTypeInfo(node).Type is not {IsTupleType: true}) return null; + if (GetPlainTupleCreationTypeOrNull(node) is null) return null; if (node.ArgumentList?.Arguments.All(a => a is VBSyntax.SimpleArgumentSyntax {IsNamed: false}) != true) return null; var elements = node.ArgumentList.Arguments.Cast().ToList(); @@ -57,6 +57,13 @@ public async Task ConvertOrNullAsync(VBSyntax.ObjectCreationEx return restElements is {Count: > 0} ? elements.Take(RestIndex).Concat(restElements).ToList() : null; } - private bool HasNoConversion(VBSyntax.ExpressionSyntax expression) => - _semanticModel.GetTypeInfo(expression) is var typeInfo && SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType); + /// The created tuple type, unless an object initializer sets members after construction. + private ITypeSymbol GetPlainTupleCreationTypeOrNull(VBSyntax.ObjectCreationExpressionSyntax node) => + node.Initializer == null && _semanticModel.GetTypeInfo(node).Type is {IsTupleType: true} tupleType ? tupleType : null; + + private bool HasNoConversion(VBSyntax.ExpressionSyntax expression) + { + var typeInfo = _semanticModel.GetTypeInfo(expression); + return SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType); + } } From a3b9cd104aa1096874c8fd71ddbad1e78cfaca45 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 16:50:28 +0000 Subject: [PATCH 9/9] Test tuple literals with Option Strict Off and enum element conversions Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/ExpressionTests/ExpressionTests.cs | 35 +++++++++++++++++++ .../VBToCS/ValueTupleCreationTests.vb | 18 ++++++++++ 2 files changed, 53 insertions(+) diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs index c7e714849..71bb6694f 100644 --- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs @@ -2419,6 +2419,41 @@ await TestConversionVisualBasicToCSharpAsync( }"); } + [Fact] + public async Task ValueTupleConstructorWithConvertedElementsBecomesTupleLiteralAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Option Strict Off + +Enum Colour + Red +End Enum + +Class C + Sub Foo(o As Object, c As Colour) + Dim narrowed = New ValueTuple(Of Integer, String)(o, o) + Dim enums = New ValueTuple(Of Colour, Integer)(c, c) + Dim enumFromInteger = New ValueTuple(Of Colour, Colour)(0, c) + End Sub +End Class", + @"using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +internal enum Colour +{ + Red +} + +internal partial class C +{ + public void Foo(object o, Colour c) + { + var narrowed = (Conversions.ToInteger(o), Conversions.ToString(o)); + var enums = (c, (int)c); + var enumFromInteger = ((Colour)0, c); + } +}", incompatibleWithAutomatedCommentTesting: true); // Option statement comments are lost + } + [Fact] public async Task ValueTupleConstructorWithoutArgumentsBecomesDefaultAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb index 555748d62..a72541fce 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb @@ -30,6 +30,24 @@ Public Class ValueTupleCreationTests Assert.Equal(("a", "b"c), stringAndChar) End Sub + Private Enum Colour + Red + Green + End Enum + + + Sub ConvertedElementsKeepTheTypeArguments() + Dim o As Object = "3" + Dim narrowed = New ValueTuple(Of Integer, String)(CInt(o), CStr(o)) + Assert.Equal(GetType(ValueTuple(Of Integer, String)), narrowed.GetType()) + Assert.Equal((3, "3"), narrowed) + + Dim c = Colour.Green + Dim enums = New ValueTuple(Of Colour, Integer)(c, c) + Assert.Equal(GetType(ValueTuple(Of Colour, Integer)), enums.GetType()) + Assert.Equal((Colour.Green, 1), enums) + End Sub + Sub ElementsNeedingATargetTypeKeepTheTypeArguments() Dim x As Integer = 1