// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeQuality.Analyzers.QualityGuidelines.AvoidMultipleEnumerations.FlowAnalysis;
namespace Microsoft.CodeQuality.Analyzers.QualityGuidelines.AvoidMultipleEnumerations
{
internal static class AvoidMultipleEnumerationsHelpers
{
/// <summary>
/// Skip the deferred method call and conversion operation in Linq methods call chain.
/// Return the tail of the Linq chain operation, and possible enumerationCount by the Linq Chain Method.
/// </summary>
public static (IOperation linqChainTailOperation, EnumerationCount linqChainEnumerationCount) SkipLinqChainAndConversionMethod(
IOperation operation,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
RoslynDebug.Assert(operation is IParameterReferenceOperation or ILocalReferenceOperation);
return VisitLinqChainAndCoversionMethod(operation, EnumerationCount.Zero, wellKnownSymbolsInfo);
static (IOperation linqChainTailOperation, EnumerationCount enumerationCount) VisitLinqChainAndCoversionMethod(
IOperation operation,
EnumerationCount enumerationCount,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
if (IsValidImplicitConversion(operation.Parent, wellKnownSymbolsInfo))
{
// Go to the implicit conversion if needed
// e.g.
// void Bar (IOrderedEnumerable<T> c)
// {
// c.First();
// }
// here 'c' would be converted to IEnumerable<T>
return VisitLinqChainAndCoversionMethod(operation.Parent, enumerationCount, wellKnownSymbolsInfo);
}
if (IsOperationIsArgumentOfLinqChainInvocation(operation.Parent, wellKnownSymbolsInfo, out var enumerateArgument))
{
// This operation is used as an argument of a deferred execution method.
// Check if the invocation of the deferred execution method is used in another deferred execution method.
return VisitLinqChainAndCoversionMethod(
operation.Parent.Parent!,
enumerateArgument
? InvocationSetHelpers.AddInvocationCount(enumerationCount, EnumerationCount.One)
: enumerationCount,
wellKnownSymbolsInfo);
}
if (IsInstanceOfLinqChainInvocation(operation, wellKnownSymbolsInfo, out var enumerateInstance))
{
// If the extension method could be used as reduced method, also check its invocation instance.
// Like in VB,
// 'i.Select(Function(a) a)', 'i' is the invocation instance of 'Select'
return VisitLinqChainAndCoversionMethod(
operation.Parent!,
enumerateInstance
? InvocationSetHelpers.AddInvocationCount(enumerationCount, EnumerationCount.One)
: enumerationCount,
wellKnownSymbolsInfo);
}
return (operation, enumerationCount);
}
}
public static bool IsValidImplicitConversion([NotNullWhen(true)] IOperation? operation, WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
// Check if this is an implicit conversion operation convert from one delay type to another delay type.
// This is used in methods chain like
// 1. Cast<T> and OfType<T>, which takes IEnumerable as the first parameter. For example:
// c.Select(i => i + 1).Cast<long>();
// 'c.Select(i => i + 1)' has IEnumerable<T> type, and will be implicitly converted to IEnumerable. Then the conversion result would be passed to Cast<long>().
// 2. OrderBy, ThenBy, etc.. which returns IOrderedIEnumerable<T>. For this example,
// c.OrderBy(i => i.Key).Select(m => m + 1);
// 'c.OrderBy(i => i.Key)' has IOrderedIEnumerable<T> type, and will be implicitly converted to IEnumerable<T> . Then the conversion result would be passed to Select()
// 3. For each loop in C#. C# binder would create a conversion for the collection before calling GetEnumerator()
// Note: this is not true for VB, VB binder won't generate the conversion.
return operation is IConversionOperation { IsImplicit: true } conversionOperation
&& IsDeferredType(conversionOperation.Operand.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes)
&& IsDeferredType(conversionOperation.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes);
}
/// <summary>
/// Check if the operation is deferred type and also it is a collection enumerated by a for each loop.
/// </summary>
public static bool IsOperationEnumeratedByForEachLoop(
IOperation operation,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
if (!IsDeferredType(operation.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
return operation.Parent is IForEachLoopOperation forEachLoopOperation && forEachLoopOperation.Collection == operation;
}
private static bool IsInstanceOfLinqChainInvocation(
IOperation operation, WellKnownSymbolsInfo wellKnownSymbolsInfo, out bool enumerateInstance)
{
if (operation.Parent is IInvocationOperation invocationOperation
&& invocationOperation.Instance == operation
&& IsLinqChainInvocation(invocationOperation, wellKnownSymbolsInfo, out enumerateInstance))
{
return true;
}
enumerateInstance = false;
return false;
}
public static bool IsOperationEnumeratedByInvocation(
IOperation operation,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
// Case 1:
// For C# or the method is called as an ordinary method,
// 'i.ElementAt(10)', is essentially 'ElementAt(i, 10)'
if (operation.Parent is IArgumentOperation parentArgumentOperation)
{
if (parentArgumentOperation.Parent is IInvocationOperation grandParentInvocationOperation)
{
return IsInvocationCausingEnumerationOverArgument(
grandParentInvocationOperation,
parentArgumentOperation,
wellKnownSymbolsInfo);
}
else if (parentArgumentOperation.Parent is IObjectCreationOperation grandParentObjectCreationOperation)
{
return IsObjectCreationOperationCausingEnumerationOverArgument(
grandParentObjectCreationOperation,
parentArgumentOperation,
wellKnownSymbolsInfo);
}
}
// Case 2:
// If the method is in reduced form.
// Like in VB,
// 'i.ElementAt(10)', 'i' is thought as the invocation instance.
if (operation.Parent is IInvocationOperation { TargetMethod.MethodKind: MethodKind.ReducedExtension } parentInvocationOperation
&& operation == parentInvocationOperation.Instance)
{
return IsInvocationCausingEnumerationOverInvocationInstance(parentInvocationOperation, wellKnownSymbolsInfo);
}
return false;
}
/// <summary>
/// Get the original parameter symbol in the ReducedFromMethod.
/// </summary>
private static IParameterSymbol GetReducedFromParameter(IMethodSymbol methodSymbol, IParameterSymbol parameterSymbol)
{
RoslynDebug.Assert(methodSymbol.Parameters.Contains(parameterSymbol));
RoslynDebug.Assert(methodSymbol.ReducedFrom != null);
var reducedFromMethodSymbol = methodSymbol.ReducedFrom.OriginalDefinition;
var index = methodSymbol.Parameters.IndexOf(parameterSymbol);
return reducedFromMethodSymbol.Parameters[index + 1];
}
/// <summary>
/// Return true if the target method of the <param name="invocationOperation"/> is a reduced extension method, and it will enumerate its invocation instance.
/// </summary>
private static bool IsInvocationCausingEnumerationOverInvocationInstance(IInvocationOperation invocationOperation, WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
if (invocationOperation.Instance == null
|| invocationOperation.TargetMethod.MethodKind != MethodKind.ReducedExtension
|| !IsDeferredType(invocationOperation.Instance.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
var originalTargetMethod = invocationOperation.TargetMethod.ReducedFrom!.OriginalDefinition;
// Well-known linq methods, like 'TryGetNonEnumeratedCount'
if (originalTargetMethod.Parameters.IsEmpty || wellKnownSymbolsInfo.NoEnumerationMethods.Contains(originalTargetMethod))
{
return false;
}
// Well-known linq methods, like 'ElementAt'
if (wellKnownSymbolsInfo.EnumeratedMethods.Contains(originalTargetMethod))
{
return true;
}
// User defined method from editor config
return wellKnownSymbolsInfo.IsCustomizedLinqChainMethods(originalTargetMethod);
}
/// <summary>
/// Check if <param name="invocationOperation"/> is targeting a method that will cause the enumeration of <param name="argumentOperationToCheck"/>.
/// </summary>
private static bool IsInvocationCausingEnumerationOverArgument(
IInvocationOperation invocationOperation,
IArgumentOperation argumentOperationToCheck,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
RoslynDebug.Assert(invocationOperation.Arguments.Contains(argumentOperationToCheck));
RoslynDebug.Assert(!IsLinqChainInvocation(invocationOperation, argumentOperationToCheck, wellKnownSymbolsInfo, out _));
return IsInvokingMethodEnumeratedOverArgument(invocationOperation.TargetMethod, argumentOperationToCheck, wellKnownSymbolsInfo);
}
private static bool IsObjectCreationOperationCausingEnumerationOverArgument(
IObjectCreationOperation objectCreationOperation,
IArgumentOperation argumentOperation,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
RoslynDebug.Assert(objectCreationOperation.Arguments.Contains(argumentOperation));
return IsInvokingMethodEnumeratedOverArgument(objectCreationOperation.Constructor, argumentOperation, wellKnownSymbolsInfo);
}
private static bool IsInvokingMethodEnumeratedOverArgument(
IMethodSymbol? invokingMethod,
IArgumentOperation argumentOperation,
WellKnownSymbolsInfo wellKnownSymbolsInfo)
{
if (invokingMethod == null ||
argumentOperation.Parameter == null ||
!IsDeferredType(argumentOperation.Value.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
var reducedFromMethod = invokingMethod.ReducedFrom ?? invokingMethod;
var originalMethod = reducedFromMethod.OriginalDefinition;
if (wellKnownSymbolsInfo.NoEnumerationMethods.Contains(originalMethod))
{
return false;
}
var argumentMappingParameter = invokingMethod.MethodKind == MethodKind.ReducedExtension
? GetReducedFromParameter(invokingMethod, argumentOperation.Parameter)
: argumentOperation.Parameter.OriginalDefinition;
// Common linq method case, like ElementAt
if (wellKnownSymbolsInfo.EnumeratedMethods.Contains(originalMethod)
&& originalMethod.Parameters.Any(
methodParameter => IsDeferredType(methodParameter.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes) && methodParameter.Equals(argumentMappingParameter)))
{
return true;
}
// Enumeration methods specified in editorConfig
if (wellKnownSymbolsInfo.IsCustomizedEnumerationMethods(originalMethod))
{
return true;
}
// Analyzer is in aggressive mode, assuming all methods enumerated the argument if we know the type of mapping parameter is IEnumerable type.
return wellKnownSymbolsInfo.AssumeMethodEnumeratesParameters
&& IsDeferredType(argumentMappingParameter.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes);
}
/// <summary>
/// Check if <param name="operation"/> is an argument that passed into a linq chain. (like Select, Where etc.)
/// </summary>
private static bool IsOperationIsArgumentOfLinqChainInvocation(
[NotNullWhen(true)] IOperation? operation, WellKnownSymbolsInfo wellKnownSymbolsInfo, out bool enumerateArgument)
{
if (operation is IArgumentOperation { Parent: IInvocationOperation invocationOperation } argumentOperation)
{
return IsLinqChainInvocation(invocationOperation, argumentOperation, wellKnownSymbolsInfo, out enumerateArgument);
}
enumerateArgument = false;
return false;
}
/// <summary>
/// Check if <param name="argumentOperationToCheck"/> is passed as a deferred executing argument into <param name="invocationOperation"/>.
/// </summary>
public static bool IsLinqChainInvocation(
IInvocationOperation invocationOperation,
IArgumentOperation argumentOperationToCheck,
WellKnownSymbolsInfo wellKnownSymbolsInfo,
out bool enumerateArgument)
{
enumerateArgument = false;
RoslynDebug.Assert(invocationOperation.Arguments.Contains(argumentOperationToCheck));
if (argumentOperationToCheck.Parameter == null ||
!IsDeferredType(argumentOperationToCheck.Value.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
var targetMethod = invocationOperation.TargetMethod;
// For C#, extension method is used as an ordinary static method.
// For VB, ex: a.Concat(b)
// 'b' is an argument to 'Concat', which is a reduced method.
var reducedFromMethod = targetMethod.ReducedFrom ?? targetMethod;
var originalMethod = reducedFromMethod.OriginalDefinition;
if (!IsDeferredType(targetMethod.ReturnType.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
// For VB, ex: a.Concat(b)
// 'b' is in fact the first argument to 'Concat', because the extension method in VB is reduced.
var argumentMappingParameter = targetMethod.MethodKind == MethodKind.ReducedExtension
? GetReducedFromParameter(targetMethod, argumentOperationToCheck.Parameter)
: argumentOperationToCheck.Parameter;
if (wellKnownSymbolsInfo.LinqChainMethods.Contains(originalMethod)
&& originalMethod.Parameters.Any(
methodParameter => IsDeferredType(methodParameter.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes) && methodParameter.Equals(argumentMappingParameter.OriginalDefinition)))
{
// All well-known linq chain methods under Linq namespace won't enumerate the argument.
// e.g. For methods like 'Select', 'Where', etc..
// call 'a.Select(i => i + 1)' won't enumerate 'a'
return true;
}
if (wellKnownSymbolsInfo.IsCustomizedLinqChainMethods(originalMethod))
{
enumerateArgument = wellKnownSymbolsInfo.IsCustomizedEnumerationMethods(originalMethod);
return true;
}
return false;
}
/// <summary>
/// Return true if the TargetMethod of <param name="invocationOperation"/> is a reduced extension method, and is a Linq chain methods
/// </summary>
public static bool IsLinqChainInvocation(IInvocationOperation invocationOperation, WellKnownSymbolsInfo wellKnownSymbolsInfo, out bool enumerateInstance)
{
enumerateInstance = false;
if (invocationOperation.Instance == null
|| invocationOperation.TargetMethod.MethodKind != MethodKind.ReducedExtension
|| !IsDeferredType(invocationOperation.Instance.Type?.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes)
|| !IsDeferredType(invocationOperation.TargetMethod.ReturnType.OriginalDefinition, wellKnownSymbolsInfo.AdditionalDeferredTypes))
{
return false;
}
var originalMethod = invocationOperation.TargetMethod.ReducedFrom!.OriginalDefinition;
if (wellKnownSymbolsInfo.LinqChainMethods.Contains(originalMethod))
{
return true;
}
if (wellKnownSymbolsInfo.IsCustomizedLinqChainMethods(originalMethod))
{
enumerateInstance = wellKnownSymbolsInfo.IsCustomizedEnumerationMethods(originalMethod);
return true;
}
return false;
}
public static ImmutableArray<IMethodSymbol> GetEnumeratedMethods(WellKnownTypeProvider wellKnownTypeProvider,
ImmutableArray<(string typeName, string methodName)> typeAndMethodNames,
ImmutableArray<string> linqMethodNames,
ImmutableArray<string> constructorTypeNames)
{
using var builder = ArrayBuilder<IMethodSymbol>.GetInstance();
GetImmutableCollectionConversionMethods(wellKnownTypeProvider, typeAndMethodNames, builder);
GetWellKnownMethods(wellKnownTypeProvider, WellKnownTypeNames.SystemLinqEnumerable, linqMethodNames, builder);
GetConstructors(wellKnownTypeProvider, constructorTypeNames, builder);
return builder.ToImmutable();
}
public static void GetConstructors(
WellKnownTypeProvider wellKnownTypeProvider,
ImmutableArray<string> typeNames,
ArrayBuilder<IMethodSymbol> builder)
{
foreach (var typeName in typeNames)
{
if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(typeName, out var type))
{
builder.AddRange(type.Constructors.Where(c => c.Parameters.Any(p => p.Type.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)));
}
}
}
private static void GetImmutableCollectionConversionMethods(
WellKnownTypeProvider wellKnownTypeProvider,
ImmutableArray<(string, string)> typeAndMethodNames,
ArrayBuilder<IMethodSymbol> builder)
{
// Get immutable collection conversion method, like ToImmutableArray()
foreach (var (typeName, methodName) in typeAndMethodNames)
{
if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(typeName, out var type))
{
var methods = type.GetMembers(methodName);
foreach (var method in methods)
{
// Usually there are two overloads for these methods, like ToImmutableArray,
// it has two overloads, one convert from ImmutableArray.Builder and one convert from IEnumerable<T>
// and we only want the last one
if (method is IMethodSymbol { Parameters: { Length: > 0 } parameters, IsExtensionMethod: true } methodSymbol
&& parameters[0].Type.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
{
builder.Add(methodSymbol);
}
}
}
}
}
public static ImmutableArray<IMethodSymbol> GetGetEnumeratorMethods(WellKnownTypeProvider wellKnownTypeProvider)
{
using var builder = ArrayBuilder<IMethodSymbol>.GetInstance();
if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsIEnumerable, out var nonGenericIEnumerable))
{
var method = nonGenericIEnumerable.GetMembers(WellKnownMemberNames.GetEnumeratorMethodName).FirstOrDefault();
if (method is IMethodSymbol methodSymbol)
{
builder.Add(methodSymbol);
}
}
if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1, out var genericIEnumerable))
{
var method = genericIEnumerable.GetMembers(WellKnownMemberNames.GetEnumeratorMethodName).FirstOrDefault();
if (method is IMethodSymbol methodSymbol)
{
builder.Add(methodSymbol);
}
}
return builder.ToImmutable();
}
private static bool IsConstraintTypesHasDeferredType(ITypeParameterSymbol typeParameterSymbol, ImmutableArray<ITypeSymbol> additionalTypesToCheck)
=> typeParameterSymbol.ConstraintTypes.Any(type => IsDeferredType(type?.OriginalDefinition, additionalTypesToCheck));
public static bool IsDeferredType(ITypeSymbol? type, ImmutableArray<ITypeSymbol> additionalTypesToCheck)
=> type switch
{
null => false,
ITypeParameterSymbol typeParameterSymbol => IsConstraintTypesHasDeferredType(typeParameterSymbol, additionalTypesToCheck),
_ => type.SpecialType is SpecialType.System_Collections_Generic_IEnumerable_T or SpecialType.System_Collections_IEnumerable || additionalTypesToCheck.Contains(type)
};
public static ImmutableArray<ITypeSymbol> GetTypes(Compilation compilation, ImmutableArray<string> typeNames)
{
using var builder = ArrayBuilder<ITypeSymbol>.GetInstance();
foreach (var name in typeNames)
{
if (compilation.TryGetOrCreateTypeByMetadataName(name, out var typeSymbol))
{
builder.Add(typeSymbol);
}
}
return builder.ToImmutable();
}
public static ImmutableArray<IMethodSymbol> GetLinqMethods(WellKnownTypeProvider wellKnownTypeProvider, ImmutableArray<string> methodNames)
{
using var builder = ArrayBuilder<IMethodSymbol>.GetInstance();
GetWellKnownMethods(wellKnownTypeProvider, WellKnownTypeNames.SystemLinqEnumerable, methodNames, builder);
return builder.ToImmutable();
}
private static void GetWellKnownMethods(
WellKnownTypeProvider wellKnownTypeProvider,
string typeName,
ImmutableArray<string> methodNames,
ArrayBuilder<IMethodSymbol> builder)
{
if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(typeName, out var type))
{
foreach (var methodSymbol in type.GetMembers().OfType<IMethodSymbol>())
{
if (methodNames.Contains(methodSymbol.Name))
{
builder.Add(methodSymbol);
}
}
}
}
}
}