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