// 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.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.NetCore.Analyzers.Runtime
{
using static MicrosoftNetCoreAnalyzersResources;
/// <summary>
/// CA2021: Do not call Enumerable.Cast or Enumerable.OfType with incompatible types.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA2021";
private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesTitle));
private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesDescription));
private static readonly LocalizableString s_localizableCastMessage = CreateLocalizableResourceString(nameof(DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesMessageCast));
private static readonly LocalizableString s_localizableOfTypeMessage = CreateLocalizableResourceString(nameof(DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesMessageOfType));
internal static DiagnosticDescriptor CastRule = DiagnosticDescriptorHelper.Create(RuleId,
s_localizableTitle,
s_localizableCastMessage,
DiagnosticCategory.Reliability,
RuleLevel.BuildWarning,
s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);
internal static DiagnosticDescriptor OfTypeRule = DiagnosticDescriptorHelper.Create(RuleId,
s_localizableTitle,
s_localizableOfTypeMessage,
DiagnosticCategory.Reliability,
RuleLevel.BuildWarning,
s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);
private static readonly ImmutableArray<(string MethodName, DiagnosticDescriptor Rule)> s_methodMetadataNames = ImmutableArray.Create(
(nameof(Enumerable.Cast), CastRule),
(nameof(Enumerable.OfType), OfTypeRule)
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(OfTypeRule, CastRule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(context =>
{
if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqEnumerable, out var enumerableType))
{
return;
}
#pragma warning disable IDE0004 // Remove Unnecessary Cast - Removal of cast leads to CS8714 compiler warning.
var methodRuleDictionary = s_methodMetadataNames
.SelectMany(m => enumerableType
.GetMembers(m.MethodName)
.OfType<IMethodSymbol>()
.Where(method => method.IsExtensionMethod
&& method.TypeParameters.HasExactly(1)
&& method.Parameters.HasExactly(1)
&& method.Parameters[0].Type.OriginalDefinition.SpecialType
== SpecialType.System_Collections_IEnumerable
)
.Select(method => (method, m.Rule)))
.ToImmutableDictionary(key => (ISymbol)key.method, v => v.Rule, SymbolEqualityComparer.Default);
#pragma warning restore IDE0004 // Remove Unnecessary Cast
if (methodRuleDictionary.IsEmpty)
{
return;
}
context.RegisterOperationAction(context =>
{
var invocation = (IInvocationOperation)context.Operation;
var targetMethod = (invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod).OriginalDefinition;
if (!methodRuleDictionary.TryGetValue(targetMethod, out var rule))
{
return;
}
var instanceArg = invocation.GetInstance(); // "this" argument of an extension method
static ITypeSymbol? GetIEnumerableTParam(ITypeSymbol type)
{
if (type is not INamedTypeSymbol argIEnumerableType
|| !argIEnumerableType.TypeArguments.HasExactly(1))
{
return null;
}
return argIEnumerableType.TypeArguments[0];
}
static ITypeSymbol? FindElementType(IOperation? operation)
{
if (operation?.Type is null)
{
return null;
}
if (operation.Kind == OperationKind.ArrayCreation)
{
return (operation.Type as IArrayTypeSymbol)?.ElementType;
}
if (operation.Type.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
{
return GetIEnumerableTParam(operation.Type);
}
INamedTypeSymbol? enumerableInterface = null;
foreach (var t in operation.Type.AllInterfaces)
{
if (t.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
{
if (enumerableInterface is not null)
{
return null; // if the type implements IEnumerable<T> multiple times, give up
}
enumerableInterface = t;
}
}
if (enumerableInterface is not null)
{
return GetIEnumerableTParam(enumerableInterface);
}
if (operation is IParenthesizedOperation parenthesizedOperation)
{
return FindElementType(parenthesizedOperation.Operand);
}
if (operation is IConversionOperation conversionOperation
&& conversionOperation.OperatorMethod is null) // implicit meaning 'not user defined'
{
return FindElementType(conversionOperation.Operand);
}
return null;
}
// because the type of the parameter is actually the non-generic IEnumerable,
// we have to reach back through conversion operator(s) to get the element type
var castFrom = FindElementType(instanceArg);
if (castFrom is null)
{
return;
}
if (!invocation.TargetMethod.TypeArguments.HasExactly(1))
{
return;
}
var castTo = invocation.TargetMethod.TypeArguments[0];
if (CastWillAlwaysFail(castFrom, castTo))
{
context.ReportDiagnostic(invocation.CreateDiagnostic(rule, castFrom.ToDisplayString(), castTo.ToDisplayString()));
}
}, OperationKind.Invocation);
});
// because this is a warning, we want to be very sure
// this won't catch all problems, but it should never report something
// as a problem in correctly. We don't want another IDE0004
static bool CastWillAlwaysFail(ITypeSymbol castFrom, ITypeSymbol castTo)
{
castFrom = castFrom.GetNullableValueTypeUnderlyingType()
?? castFrom.GetUnderlyingValueTupleTypeOrThis()!;
castTo = castTo.GetNullableValueTypeUnderlyingType()
?? castTo.GetUnderlyingValueTupleTypeOrThis()!;
if (castFrom.TypeKind == TypeKind.Error
|| castTo.TypeKind == TypeKind.Error)
{
return false;
}
// Most checks are better with OriginalDefinition, but keep the ones passed in around.
ITypeSymbol castFromParam = castFrom;
ITypeSymbol castToParam = castTo;
castFrom = castFrom.OriginalDefinition;
castTo = castTo.OriginalDefinition;
if (castFrom.SpecialType == SpecialType.System_Object
|| castTo.SpecialType == SpecialType.System_Object)
{
// some things will actually fail, eg. TypedReference
// but they should be pretty rare
return false;
}
if (castFrom.Equals(castTo, SymbolEqualityComparer.Default))
{
return false;
}
static bool CastToTypeParamWillAlwaysFail(ITypeSymbol castFrom, ITypeParameterSymbol castToTypeParam)
{
if (castToTypeParam.HasValueTypeConstraint
&& ValueTypeConstraintImpossible(castFrom))
{
return true;
}
// because object is a reference type the 'class' reference type constraint
// doesn't actually constrain unless a type is specified too
// not implemented:
// NotNullConstraint
// ConstructorConstraint
// UnmanagedTypeConstraint
// Nullability annotations
if (castToTypeParam.ConstraintTypes.Any(constraintType => CastWillAlwaysFail(castFrom, constraintType)))
{
return true;
}
return false;
}
static bool ValueTypeConstraintImpossible(ITypeSymbol t)
{
if (t.TypeKind == TypeKind.Class)
{
return t.SpecialType is not SpecialType.System_Enum
and not SpecialType.System_ValueType;
}
return false;
}
switch (castFrom.TypeKind, castTo.TypeKind)
{
case (TypeKind.Dynamic, _):
case (_, TypeKind.Dynamic):
return false;
case (TypeKind.TypeParameter, _):
var castFromTypeParam = (ITypeParameterSymbol)castFrom;
if (castFromTypeParam.ConstraintTypes.Any(constraintType => CastWillAlwaysFail(constraintType, castTo)))
{
return true;
}
if (castFromTypeParam.HasValueTypeConstraint
&& ValueTypeConstraintImpossible(castTo))
{
return true;
}
if (castTo.TypeKind == TypeKind.TypeParameter)
{
return CastToTypeParamWillAlwaysFail(castFrom, (ITypeParameterSymbol)castTo);
}
return false;
case (_, TypeKind.TypeParameter):
return CastToTypeParamWillAlwaysFail(castFrom, (ITypeParameterSymbol)castTo);
case (TypeKind.Class, TypeKind.Class):
return !castFromParam.DerivesFrom(castToParam)
&& !castToParam.DerivesFrom(castFromParam);
case (TypeKind.Interface, TypeKind.Class):
return castTo.IsSealed && !castTo.DerivesFrom(castFrom);
case (TypeKind.Class, TypeKind.Interface):
return castFrom.IsSealed && !castFrom.DerivesFrom(castTo);
case (TypeKind.Interface, TypeKind.Struct):
return !castTo.DerivesFrom(castFrom);
case (TypeKind.Struct, TypeKind.Interface):
return !castFrom.DerivesFrom(castTo);
case (TypeKind.Class, TypeKind.Enum):
return castFrom.SpecialType is not SpecialType.System_Enum
and not SpecialType.System_ValueType;
case (TypeKind.Enum, TypeKind.Class):
return castTo.SpecialType is not SpecialType.System_Enum
and not SpecialType.System_ValueType;
case (TypeKind.Struct, TypeKind.Enum)
when castTo is INamedTypeSymbol toEnum:
return !castFrom.Equals(toEnum.EnumUnderlyingType);
case (TypeKind.Enum, TypeKind.Struct)
when castFrom is INamedTypeSymbol fromEnum:
return !fromEnum.EnumUnderlyingType!.Equals(castTo);
case (TypeKind.Enum, TypeKind.Enum)
when castFrom is INamedTypeSymbol fromEnum
&& castTo is INamedTypeSymbol toEnum:
return !fromEnum.EnumUnderlyingType!.Equals(toEnum.EnumUnderlyingType);
// this is too conservative
// array variance is not implemented
// - eg. object[] -> class[]
// boxing shouldn't be allowed
// - eg. object[] -> ValueType[]
case (TypeKind.Array, TypeKind.Array)
when castFrom is IArrayTypeSymbol fromArray
&& castTo is IArrayTypeSymbol toArray:
return fromArray.Rank != toArray.Rank
|| CastWillAlwaysFail(fromArray.ElementType, toArray.ElementType);
case (TypeKind.Array, TypeKind.Class):
return castTo.SpecialType != SpecialType.System_Array;
case (TypeKind.Class, TypeKind.Array):
return castFrom.SpecialType != SpecialType.System_Array;
case (TypeKind.Class, TypeKind.Struct):
return castFrom.SpecialType != SpecialType.System_ValueType;
case (TypeKind.Struct, TypeKind.Class):
return castTo.SpecialType != SpecialType.System_ValueType;
case (_, TypeKind.Enum):
case (TypeKind.Enum, _):
case (_, TypeKind.Struct):
case (TypeKind.Struct, _):
return true;
case (TypeKind.Interface, TypeKind.Interface):
default:
return false; // we don't *know* it'll fail...
}
}
}
}
}