diff --git a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs index c7efe2e32..eb6de7f58 100644 --- a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs @@ -256,6 +256,62 @@ await WithRemovedRedundantConversionOrNullAsync(node, expressionSymbol)) is { } return await ConvertInvocationAsync(node, invocationSymbol, expressionSymbol); } + /// + /// 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. + /// + 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)); + } + + /// + /// VB fixes some type parameters from the receiver while reducing the method, and infers the rest from the arguments + /// + 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); + + /// + /// The argument implements the parameter's generic type with different type arguments, e.g. IEqualityComparer(Of Base) for IEqualityComparer(Of Derived) + /// + 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 WithRemovedRedundantConversionOrNullAsync(VBSyntax.InvocationExpressionSyntax conversionNode, ISymbol invocationSymbol) { if (invocationSymbol?.ContainingNamespace.MetadataName != nameof(Microsoft.VisualBasic) || @@ -334,6 +390,8 @@ private async Task 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); } diff --git a/Tests/CSharp/ExpressionTests/LinqExpressionTests.cs b/Tests/CSharp/ExpressionTests/LinqExpressionTests.cs index 6df8eefb4..4ce0c82a4 100644 --- a/Tests/CSharp/ExpressionTests/LinqExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/LinqExpressionTests.cs @@ -959,6 +959,166 @@ public IQueryable M(IQueryable rs) where r.B == true && r.N > 1 || r.N2 > 1 && r.B == true select r; } +}"); + } + + /// + /// VB infers Distinct's type argument from the receiver first, whereas C# would infer BaseValue from the comparer too + /// + [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 ToDictionary(IEnumerable values) + { + return values.Distinct(EqualityComparer.Default).ToDictionary(value => value.Key); + } +}"); + } + + /// + /// The type argument VB inferred can be the calling method's own type parameter + /// + [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 DistinctValues(IEnumerable values) where T : BaseValue + { + return values.Distinct(EqualityComparer.Default).ToList(); + } +}"); + } + + /// + /// Conditional access converts to a member binding, which needs the same type arguments + /// + [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 DistinctValues(IEnumerable values) + { + return values?.Distinct(EqualityComparer.Default).ToList(); + } +}"); + } + + /// + /// C# infers the same type arguments when an argument implements the generic parameter type exactly + /// + [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 ByName(IEnumerable values) + { + return values.ToDictionary(v => v.Name, EqualityComparer.Default); + } }"); } } diff --git a/Tests/CSharp/ExpressionTests/LinqQueryGroupByTests.cs b/Tests/CSharp/ExpressionTests/LinqQueryGroupByTests.cs index cd10d7cfb..6672a9d1a 100644 --- a/Tests/CSharp/ExpressionTests/LinqQueryGroupByTests.cs +++ b/Tests/CSharp/ExpressionTests/LinqQueryGroupByTests.cs @@ -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 policies, IQueryable 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(); + } }"); } } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/LinqExpressionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/LinqExpressionTests.vb index ea39a038d..2d5e0c22b 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/LinqExpressionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/LinqExpressionTests.vb @@ -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 + + + 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 + + + Sub TestGenericMethodInferenceWithBaseTypeComparer() + Dim value = New DerivedValue With {.Key = 1} + Dim result = ToDictionary({value, value}) + Assert.Same(value, result(1)) + End Sub + End Class