Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CodeConverter/CSharp/NameExpressionNodeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,62 @@ await WithRemovedRedundantConversionOrNullAsync(node, expressionSymbol)) is { }
return await ConvertInvocationAsync(node, invocationSymbol, expressionSymbol);
}

/// <summary>
/// VB infers an extension method's type arguments from the receiver first, then converts the other arguments to fit,
/// whereas C# infers from all arguments together, e.g. `values.Distinct(EqualityComparer(Of BaseValue).Default)` would make C# infer BaseValue.
/// So where an argument only fits its generic parameter via a variance conversion, state the type arguments VB inferred.
/// </summary>
private ExpressionSyntax WithTypeArgumentsInferredByVb(IOperation operation, ExpressionSyntax convertedExpression)
{
if (operation is not IInvocationOperation { TargetMethod: var method } invocation ||
(method.ReducedFrom ?? method.ConstructedFrom.ReducedFrom) is not { IsGenericMethod: true } extension ||
convertedExpression is not (MemberAccessExpressionSyntax { Name: IdentifierNameSyntax } or MemberBindingExpressionSyntax { Name: IdentifierNameSyntax }) ||
!invocation.Arguments.Any(a => IsConvertedToFitGenericParameter(a, extension))) {
return convertedExpression;
}
var typeArguments = extension.TypeParameters.Select(t => GetInferredTypeArgument(method, t)).ToList();
if (typeArguments.Any(t => t is null || t.IsAnonymousType || IsCallsOwnTypeParameter(t))) return convertedExpression;
var typeArgumentList = SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArguments.Select(t => CommonConversions.GetTypeSyntax(t))));
return convertedExpression switch {
MemberAccessExpressionSyntax memberAccess => memberAccess.WithName(SyntaxFactory.GenericName(memberAccess.Name.Identifier, typeArgumentList)),
MemberBindingExpressionSyntax memberBinding => memberBinding.WithName(SyntaxFactory.GenericName(memberBinding.Name.Identifier, typeArgumentList)),
_ => convertedExpression
};

/// The enclosing method's or type's type parameters can be named, but the call's own uninferred ones can't
bool IsCallsOwnTypeParameter(ITypeSymbol type) =>
type is ITypeParameterSymbol { DeclaringMethod: { } declaringMethod } &&
(declaringMethod.ReducedFrom != null || SymbolEqualityComparer.Default.Equals(declaringMethod.OriginalDefinition, extension.OriginalDefinition));
}

/// <summary>
/// VB fixes some type parameters from the receiver while reducing the method, and infers the rest from the arguments
/// </summary>
private static ITypeSymbol GetInferredTypeArgument(IMethodSymbol reducedMethod, ITypeParameterSymbol typeParameter) =>
reducedMethod.ConstructedFrom.GetTypeInferredDuringReduction(typeParameter) ??
reducedMethod.TypeParameters.Zip(reducedMethod.TypeArguments, (p, a) => (p, a)).FirstOrDefault(t => SymbolEqualityComparer.Default.Equals(t.p.ReducedFrom, typeParameter)).a;

private static bool IsConvertedToFitGenericParameter(IArgumentOperation argument, IMethodSymbol extension) =>
argument is { Value: IConversionOperation { IsImplicit: true, Operand.Type: { } argumentType }, Parameter: { Ordinal: var ordinal, Type: var parameterType } } &&
ordinal + 1 < extension.Parameters.Length && ContainsTypeParameter(extension.Parameters[ordinal + 1].Type) &&
IsVarianceConversion(argumentType, parameterType);

/// <summary>
/// The argument implements the parameter's generic type with different type arguments, e.g. IEqualityComparer(Of Base) for IEqualityComparer(Of Derived)
/// </summary>
private static bool IsVarianceConversion(ITypeSymbol argumentType, ITypeSymbol parameterType) =>
parameterType is INamedTypeSymbol { IsGenericType: true } genericParameter &&
argumentType.GetBaseTypesAndThis().Concat(argumentType.AllInterfaces)
.Any(t => SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, genericParameter.OriginalDefinition) &&
!SymbolEqualityComparer.Default.Equals(t, genericParameter));

private static bool ContainsTypeParameter(ITypeSymbol type) => type switch {
ITypeParameterSymbol => true,
IArrayTypeSymbol array => ContainsTypeParameter(array.ElementType),
INamedTypeSymbol named => named.TypeArguments.Any(ContainsTypeParameter),
_ => false
};

private async Task<CSharpSyntaxNode> WithRemovedRedundantConversionOrNullAsync(VBSyntax.InvocationExpressionSyntax conversionNode, ISymbol invocationSymbol)
{
if (invocationSymbol?.ContainingNamespace.MetadataName != nameof(Microsoft.VisualBasic) ||
Expand Down Expand Up @@ -334,6 +390,8 @@ private async Task<ExpressionSyntax> ConvertInvocationAsync(VBSyntax.InvocationE
}
}

convertedExpression = WithTypeArgumentsInferredByVb(operation, convertedExpression);

if (invocationSymbol is IMethodSymbol m && convertedExpression is LambdaExpressionSyntax) {
convertedExpression = SyntaxFactory.ObjectCreationExpression(CommonConversions.GetFuncTypeSyntax(expressionType, m), ExpressionSyntaxExtensions.CreateArgList(convertedExpression), null);
}
Expand Down
160 changes: 160 additions & 0 deletions Tests/CSharp/ExpressionTests/LinqExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,166 @@ public IQueryable<R> M(IQueryable<R> rs)
where r.B == true && r.N > 1 || r.N2 > 1 && r.B == true
select r;
}
}");
}

/// <summary>
/// VB infers Distinct's type argument from the receiver first, whereas C# would infer BaseValue from the comparer too
/// </summary>
[Fact]
public async Task Issue1302_GenericMethodInferenceWithBaseTypeComparerAsync()
{
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Collections.Generic
Imports System.Linq

Public Class BaseValue
Public Property Key As Integer
End Class

Public Class DerivedValue
Inherits BaseValue
End Class

Public Module Helpers
Public Function ToDictionary(values As IEnumerable(Of DerivedValue)) As Dictionary(Of Integer, DerivedValue)
Return values.Distinct(EqualityComparer(Of BaseValue).Default).ToDictionary(Function(value) value.Key)
End Function
End Module", @"using System.Collections.Generic;
using System.Linq;

public partial class BaseValue
{
public int Key { get; set; }
}

public partial class DerivedValue : BaseValue
{
}

public static partial class Helpers
{
public static Dictionary<int, DerivedValue> ToDictionary(IEnumerable<DerivedValue> values)
{
return values.Distinct<DerivedValue>(EqualityComparer<BaseValue>.Default).ToDictionary(value => value.Key);
}
}");
}

/// <summary>
/// The type argument VB inferred can be the calling method's own type parameter
/// </summary>
[Fact]
public async Task GenericMethodInferenceWithCallersTypeParameterAsync()
{
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Collections.Generic
Imports System.Linq

Public Class BaseValue
Public Property Key As Integer
End Class

Public Class DerivedValue
Inherits BaseValue
End Class

Public Module Helpers
Public Function DistinctValues(Of T As BaseValue)(values As IEnumerable(Of T)) As List(Of T)
Return values.Distinct(EqualityComparer(Of BaseValue).Default).ToList()
End Function
End Module", @"using System.Collections.Generic;
using System.Linq;

public partial class BaseValue
{
public int Key { get; set; }
}

public partial class DerivedValue : BaseValue
{
}

public static partial class Helpers
{
public static List<T> DistinctValues<T>(IEnumerable<T> values) where T : BaseValue
{
return values.Distinct<T>(EqualityComparer<BaseValue>.Default).ToList();
}
}");
}

/// <summary>
/// Conditional access converts to a member binding, which needs the same type arguments
/// </summary>
[Fact]
public async Task GenericMethodInferenceWithConditionalAccessAsync()
{
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Collections.Generic
Imports System.Linq

Public Class BaseValue
Public Property Key As Integer
End Class

Public Class DerivedValue
Inherits BaseValue
End Class

Public Module Helpers
Public Function DistinctValues(values As IEnumerable(Of DerivedValue)) As List(Of DerivedValue)
Return values?.Distinct(EqualityComparer(Of BaseValue).Default).ToList()
End Function
End Module", @"using System.Collections.Generic;
using System.Linq;

public partial class BaseValue
{
public int Key { get; set; }
}

public partial class DerivedValue : BaseValue
{
}

public static partial class Helpers
{
public static List<DerivedValue> DistinctValues(IEnumerable<DerivedValue> values)
{
return values?.Distinct<DerivedValue>(EqualityComparer<BaseValue>.Default).ToList();
}
}");
}

/// <summary>
/// C# infers the same type arguments when an argument implements the generic parameter type exactly
/// </summary>
[Fact]
public async Task GenericMethodWithInterfaceConversionKeepsTypeArgumentsInferredAsync()
{
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Collections.Generic
Imports System.Linq

Public Class Value
Public Property Name As String
End Class

Public Module Helpers
Public Function ByName(values As IEnumerable(Of Value)) As Dictionary(Of String, Value)
Return values.ToDictionary(Function(v) v.Name, EqualityComparer(Of String).Default)
End Function
End Module", @"using System.Collections.Generic;
using System.Linq;

public partial class Value
{
public string Name { get; set; }
}

public static partial class Helpers
{
public static Dictionary<string, Value> ByName(IEnumerable<Value> values)
{
return values.ToDictionary(v => v.Name, EqualityComparer<string>.Default);
}
}");
}
}
57 changes: 57 additions & 0 deletions Tests/CSharp/ExpressionTests/LinqQueryGroupByTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,63 @@ group x by x.Length into Group
orderby Count descending
select Group.Key;
}
}");
}

[Fact]
public async Task Issue932_GroupByKeysNamedDifferentlyFromTheirMembersAsync()
{
await TestConversionVisualBasicToCSharpAsync(@"Imports System.Linq
Public Class Entity
Public Property Entity_ID As Integer
Public Property First_Name As String
Public Property Last_Name As String
End Class
Public Class Policy
Public Property Agent_Entity_ID As Integer
Public Property Insured_Entity_ID As Integer
End Class
Public Class C
Sub M(policies As IQueryable(Of Policy), entities As IQueryable(Of Entity))
Dim results = (From pCurrent In policies
Join agent In entities On agent.Entity_ID Equals pCurrent.Agent_Entity_ID
Join insured In entities On insured.Entity_ID Equals pCurrent.Insured_Entity_ID
Group By
Agent_First_Name = agent.First_Name,
Agent_Last_Name = agent.Last_Name,
Insured_First_Name = insured.First_Name,
Insured_Last_Name = insured.Last_Name Into Group1 = Group
Select
Agent_First_Name = Agent_First_Name,
Agent_Last_Name = Agent_Last_Name,
Insured_First_Name = Insured_First_Name,
Insured_Last_Name = Insured_Last_Name).ToList()
End Sub
End Class", @"using System.Linq;

public partial class Entity
{
public int Entity_ID { get; set; }
public string First_Name { get; set; }
public string Last_Name { get; set; }
}

public partial class Policy
{
public int Agent_Entity_ID { get; set; }
public int Insured_Entity_ID { get; set; }
}

public partial class C
{
public void M(IQueryable<Policy> policies, IQueryable<Entity> entities)
{
var results = (from pCurrent in policies
join agent in entities on pCurrent.Agent_Entity_ID equals agent.Entity_ID
join insured in entities on pCurrent.Insured_Entity_ID equals insured.Entity_ID
group new { pCurrent, agent, insured } by new { Agent_First_Name = agent.First_Name, Agent_Last_Name = agent.Last_Name, Insured_First_Name = insured.First_Name, Insured_Last_Name = insured.Last_Name } into Group1
select new { Group1.Key.Agent_First_Name, Group1.Key.Agent_Last_Name, Group1.Key.Insured_First_Name, Group1.Key.Insured_Last_Name }).ToList();
}
}");
}
}
40 changes: 40 additions & 0 deletions Tests/TestData/SelfVerifyingTests/VBToCS/LinqExpressionTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,44 @@ Public Class LinqExpressionTests
Assert.Equal(3, total)
End Sub

Public Class Person
Public Property Id As Integer
Public Property Name As String
Public Sub New(id As Integer, name As String)
Me.Id = id
Me.Name = name
End Sub
End Class

<Fact>
Sub TestGroupByKeysNamedDifferentlyFromTheirMembers()
Dim people = {New Person(1, "a"), New Person(2, "b")}
Dim pairs = {Tuple.Create(1, 2), Tuple.Create(1, 2), Tuple.Create(2, 1)}
Dim q = From p In pairs
Join agent In people On agent.Id Equals p.Item1
Join insured In people On insured.Id Equals p.Item2
Group By Agent_Name = agent.Name, Insured_Name = insured.Name Into Group1 = Group
Select Agent_Name = Agent_Name, Insured_Name = Insured_Name, Count = Group1.Count()
Assert.Equal("{ Agent_Name = a, Insured_Name = b, Count = 2 }|{ Agent_Name = b, Insured_Name = a, Count = 1 }", Show(q))
End Sub

Public Class BaseValue
Public Property Key As Integer
End Class

Public Class DerivedValue
Inherits BaseValue
End Class

Private Shared Function ToDictionary(values As IEnumerable(Of DerivedValue)) As Dictionary(Of Integer, DerivedValue)
Return values.Distinct(EqualityComparer(Of BaseValue).Default).ToDictionary(Function(value) value.Key)
End Function

<Fact>
Sub TestGenericMethodInferenceWithBaseTypeComparer()
Dim value = New DerivedValue With {.Key = 1}
Dim result = ToDictionary({value, value})
Assert.Same(value, result(1))
End Sub

End Class
Loading