diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs
index 3f37f1985..0999dbf7e 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.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..0ce945f73 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.ConvertOrNullAsync(node) is { } tupleCreation) return tupleCreation;
+ 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/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs
index d1c3d3d24..a8e6dec15 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 => 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..24ca69b69
--- /dev/null
+++ b/CodeConverter/CSharp/ValueTupleCreationConverter.cs
@@ -0,0 +1,69 @@
+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), 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).
+/// 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 ConvertOrNullAsync(VBSyntax.ObjectCreationExpressionSyntax node)
+ {
+ 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);
+ 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.
+ /// A TRest created without arguments has no elements to flatten: the literal would lose the nested tuple.
+ ///
+ private List GetTupleElementsOrNull(VBSyntax.ObjectCreationExpressionSyntax node)
+ {
+ 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();
+ if (elements.Count <= RestIndex) return elements;
+
+ var restElements = elements[RestIndex].Expression is VBSyntax.ObjectCreationExpressionSyntax restCreation && HasNoConversion(restCreation)
+ ? GetTupleElementsOrNull(restCreation)
+ : null;
+ return restElements is {Count: > 0} ? elements.Take(RestIndex).Concat(restElements).ToList() : null;
+ }
+
+ /// 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);
+ }
+}
diff --git a/Tests/CSharp/ExpressionTests/ExpressionTests.cs b/Tests/CSharp/ExpressionTests/ExpressionTests.cs
index ef68b4387..71bb6694f 100644
--- a/Tests/CSharp/ExpressionTests/ExpressionTests.cs
+++ b/Tests/CSharp/ExpressionTests/ExpressionTests.cs
@@ -2385,6 +2385,158 @@ 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 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 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()
+ {
+ 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()
+ {
+ 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))
+ 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)
+{
+ 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));
+ 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 });
+}");
+ }
+
+ [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)
+ Dim lambda = New ValueTuple(Of Func(Of Integer), Integer)(Function() x, 2)
+End Sub",
+ @"public void Foo(string s, int x)
+{
+ var boxed = new ValueTuple