Skip to content
Draft
10 changes: 10 additions & 0 deletions CodeConverter/CSharp/CommonConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ public TypeSyntax GetTypeSyntax(ITypeSymbol typeSymbol, bool useImplicitType = f
return WithDeclarationNameCasing(syntax, typeSymbol);
}

/// <summary>
/// C# forbids tuple type syntax after <c>new</c> (CS8181), so a tuple type is named by its generic ValueTuple type there.
/// </summary>
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);
}

/// <summary>
/// Semantic model merges the symbols, but the compiled form retains multiple namespaces, which (when referenced from C#) need to keep the correct casing.
/// <seealso cref="DeclarationNodeVisitor.WithDeclarationNameCasingAsync(VBSyntax.NamespaceBlockSyntax, ISymbol)"/>
Expand Down
7 changes: 5 additions & 2 deletions CodeConverter/CSharp/ExpressionNodeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal partial class ExpressionNodeVisitor : VBasic.VisualBasicSyntaxVisitor<T
private readonly ArgumentConverter _argumentConverter;
private readonly BinaryExpressionConverter _binaryExpressionConverter;
private readonly InitializerConverter _initializerConverter;
private readonly ValueTupleCreationConverter _valueTupleCreationConverter;

public ExpressionNodeVisitor(SemanticModel semanticModel,
VisualBasicEqualityComparison visualBasicEqualityComparison, ITypeContext typeContext, CommonConversions commonConversions,
Expand All @@ -46,6 +47,7 @@ public ExpressionNodeVisitor(SemanticModel semanticModel,
_queryConverter = new QueryConverter(commonConversions, _semanticModel, TriviaConvertingExpressionVisitor, rangeVariableReplacements, _generatedNames);
_typeContext = typeContext;
_extraUsingDirectives = extraUsingDirectives;
_valueTupleCreationConverter = new ValueTupleCreationConverter(semanticModel, commonConversions);
_argumentConverter = new ArgumentConverter(visualBasicEqualityComparison, typeContext, semanticModel, commonConversions);
_xmlExpressionConverter = new XmlExpressionConverter(xmlImportContext, extraUsingDirectives, commonConversions, semanticModel, _withBlockLhs, _generatedNames, _conditionalAccessReceivers);
_nameExpressionNodeVisitor = new NameExpressionNodeVisitor(semanticModel, _generatedNames, typeContext, extraUsingDirectives, _tempNameForAnonymousScope, _withBlockLhs, commonConversions, _argumentConverter, TriviaConvertingExpressionVisitor, _conditionalAccessReceivers, rangeVariableReplacements);
Expand Down Expand Up @@ -281,9 +283,10 @@ public override async Task<CSharpSyntaxNode> VisitAnonymousObjectCreationExpress

public override async Task<CSharpSyntaxNode> 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<TypeSyntax>(TriviaConvertingExpressionVisitor),
createdType is INamedTypeSymbol {IsTupleType: true} tupleType ? CommonConversions.GetValueTupleTypeSyntax(tupleType) : await node.Type.AcceptAsync<TypeSyntax>(TriviaConvertingExpressionVisitor),
// VB can omit empty arg lists:
await _argumentConverter.ConvertArgumentListOrEmptyAsync(node, node.ArgumentList),
null
Expand Down
17 changes: 17 additions & 0 deletions CodeConverter/CSharp/TypeConversionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,23 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation,
return false;
}

/// <summary>
/// Whether the expression <see cref="AddExplicitConversion(VBSyntax.ExpressionSyntax, ExpressionSyntax, bool, bool, bool, ITypeSymbol, ITypeSymbol)"/> 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.
/// </summary>
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 {};
Expand Down
69 changes: 69 additions & 0 deletions CodeConverter/CSharp/ValueTupleCreationConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ICSharpCode.CodeConverter.CSharp;

/// <summary>
/// Converts <c>New ValueTuple(Of T1, T2)(a, b)</c> to the tuple literal <c>(a, b)</c>, and <c>New ValueTuple(Of T1, T2)()</c> to <c>default((T1, T2))</c>,
/// which create the same values. C# can't write <c>new (T1, T2)(a, b)</c> (CS8181), so anything this declines is created with <c>new ValueTuple&lt;T1, T2&gt;(a, b)</c>.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
internal class ValueTupleCreationConverter
{
/// <summary>The position of TRest in <c>ValueTuple(Of T1, ..., T7, TRest)</c>, which a longer tuple literal nests its remaining elements in.</summary>
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<ExpressionSyntax> 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<ArgumentSyntax>(_commonConversions.TriviaConvertingExpressionVisitor));
return SyntaxFactory.TupleExpression(SyntaxFactory.SeparatedList(arguments));
}

/// <summary>
/// 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.
/// </summary>
private List<VBSyntax.SimpleArgumentSyntax> 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<VBSyntax.SimpleArgumentSyntax>().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;
}

/// <summary>The created tuple type, unless an object initializer sets members after construction.</summary>
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);
}
}
152 changes: 152 additions & 0 deletions Tests/CSharp/ExpressionTests/ExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>);
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<int> 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<int, int, int, int, int, int, int, ValueTuple<int>>(1, 2, 3, 4, 5, 6, 7, rest);
var restWidened = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<long>>(1, 2, 3, 4, 5, 6, 7, new ValueTuple<int>(8));
var restDefault = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int>>(1, 2, 3, 4, 5, 6, 7, default);
var restInitialized = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int>>(1, 2, 3, 4, 5, 6, 7, new ValueTuple<int>() { 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<object, object>(s, 1);
var nullable = new ValueTuple<int?, int>(x, 1);
var named = new ValueTuple<int, string>(item2: s, item1: x);
var initialized = new ValueTuple<int, int>() { Item1 = x };
var oneElement = new ValueTuple<int>(x);
var lambda = new ValueTuple<Func<int>, 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<Func<int, (int, int)>> _create = x => new ValueTuple<int, int>(x, 2);
}");
}

[Fact]
public async Task UseEventBackingFieldAsync()
{
Expand Down
Loading
Loading