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(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); + 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); +}"); + } + [Fact] public async Task UseEventBackingFieldAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb new file mode 100644 index 000000000..a72541fce --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/ValueTupleCreationTests.vb @@ -0,0 +1,138 @@ +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 + + 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 + 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 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)) + 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()) + + 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 + + + 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