Skip to content

Commit

Permalink
fixes for CA2021 Do not call Enumerable.Cast<T> or Enumerable.OfType<…
Browse files Browse the repository at this point in the history
…T> false positives (#6459)

* fixes for CA2021 Do not call Enumerable.Cast<T> or Enumerable.OfType<T> false positives
  • Loading branch information
fowl2 authored Feb 25, 2023
1 parent 50f62a9 commit c6352bf
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CA1857 | Performance | Warning | ConstantExpectedAnalyzer, [Documentation](https
CA1858 | Performance | Info | UseStartsWithInsteadOfIndexOfComparisonWithZero, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1858)
CA1859 | Performance | Info | UseConcreteTypeAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
CA1860 | Performance | Info | PreferLengthCountIsEmptyOverAnyAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1860)
CA2021 | Reliability | Info | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)

### Removed Rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public sealed class DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer
s_localizableTitle,
s_localizableCastMessage,
DiagnosticCategory.Reliability,
RuleLevel.IdeSuggestion,
RuleLevel.BuildWarning,
s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);
Expand All @@ -39,7 +39,7 @@ public sealed class DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer
s_localizableTitle,
s_localizableOfTypeMessage,
DiagnosticCategory.Reliability,
RuleLevel.IdeSuggestion,
RuleLevel.BuildWarning,
s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);
Expand Down Expand Up @@ -118,15 +118,28 @@ public override void Initialize(AnalysisContext context)
return (operation.Type as IArrayTypeSymbol)?.ElementType;
}

if (operation.Type?.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
if (operation.Type.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
{
return GetIEnumerableTParam(operation.Type);
}

var r = operation?.Type?.AllInterfaces.FirstOrDefault(t => t.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
if (r is not null)
INamedTypeSymbol? enumerableInterface = null;
foreach (var t in operation.Type.AllInterfaces)
{
return GetIEnumerableTParam(r);
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)
Expand Down Expand Up @@ -170,6 +183,11 @@ public override void Initialize(AnalysisContext context)
// 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)
{
Expand All @@ -189,21 +207,6 @@ static bool CastWillAlwaysFail(ITypeSymbol castFrom, ITypeSymbol castTo)
return false;
}

static ITypeSymbol UnwrapNullableValueType(ITypeSymbol typeSymbol)
{
if (typeSymbol.IsValueType
&& typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T
&& ((INamedTypeSymbol)typeSymbol).TypeArguments[0] is var nullableTypeArgument)
{
return nullableTypeArgument;
}

return typeSymbol;
}

castFrom = UnwrapNullableValueType(castFrom);
castTo = UnwrapNullableValueType(castTo);

static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbol)
=> !typeParameterSymbol.HasValueTypeConstraint
&& typeParameterSymbol.ConstraintTypes.IsEmpty;
Expand All @@ -217,6 +220,10 @@ static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbo

switch (castFrom.OriginalDefinition.TypeKind, castTo.OriginalDefinition.TypeKind)
{
case (TypeKind.Dynamic, _):
case (_, TypeKind.Dynamic):
return false;

case (TypeKind.TypeParameter, _):
var castFromTypeParam = (ITypeParameterSymbol)castFrom.OriginalDefinition;
if (IsUnconstrainedTypeParameter(castFromTypeParam))
Expand Down
2 changes: 1 addition & 1 deletion src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1886,7 +1886,7 @@ Widening and user defined conversions are not supported with generic types.
|-|-|
|Category|Reliability|
|Enabled|True|
|Severity|Info|
|Severity|Warning|
|CodeFix|False|
---

Expand Down
2 changes: 1 addition & 1 deletion src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
Original file line number Diff line number Diff line change
Expand Up @@ -3382,7 +3382,7 @@
"id": "CA2021",
"shortDescription": "Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types",
"fullDescription": "Enumerable.Cast<T> and Enumerable.OfType<T> require compatible types to function expectedly. \u000aThe generic cast (IL 'unbox.any') used by the sequence returned by Enumerable.Cast<T> will throw InvalidCastException at runtime on elements of the types specified. \u000aThe generic type check (C# 'is' operator/IL 'isinst') used by Enumerable.OfType<T> will never succeed with elements of types specified, resulting in an empty sequence. \u000aWidening and user defined conversions are not supported with generic types.",
"defaultLevel": "note",
"defaultLevel": "warning",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021",
"properties": {
"category": "Reliability",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using VerifyCS = Test.Utilities.CSharpCodeFixVerifier<
Microsoft.NetCore.Analyzers.Runtime.DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer,
Expand Down Expand Up @@ -82,6 +85,94 @@ void M()
");
}

[Fact]
public async Task DynamicCSharp()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System.Linq;
class C
{
public void M()
{
dynamic x = null;
_ = x.ToString();
int[] numbers = new int[] { 1, 2, 3 };
var v = from dynamic d in numbers select (int)d;
_ = v.ToArray();
}
}
");
}

[Fact]
public async Task ValueTupleCasesCSharp()
{
var x = from (int min, int max) pair in new[] { (1, 2), (-10, -3) } select pair;
_ = x.ToArray();

await VerifyCS.VerifyAnalyzerAsync(@"
using System.Linq;
class C
{
public void M()
{
var x = from (int min, int max) pair in new[] { (1, 2), (-10, -3) } select pair;
_= x.ToArray();
}
}");
}

[Fact]
public async Task RegExCasesCSharp()
{
var actualSet = new HashSet<(int start, int end)>(
Regex.Match(".", "abc").Groups
.Cast<Group>()
.Select(g => (start: g.Index, end: g.Index + g.Length)));

await VerifyCS.VerifyAnalyzerAsync(@"
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class C
{
public void M()
{
var actualSet = new HashSet<(int start, int end)>(
Regex.Match(""."", ""abc"").Groups
.Cast<Group>()
.Select(g => (start: g.Index, end: g.Index + g.Length)));
}
}");
}

[Fact]
public async Task MultipleTypesCasesCSharp()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public sealed class MultiContainer : IEnumerable<int>, IEnumerable<long>
{
IEnumerator IEnumerable.GetEnumerator() => null;
IEnumerator<int> IEnumerable<int>.GetEnumerator() => null;
IEnumerator<long> IEnumerable<long>.GetEnumerator() => null;
public static void M()
{
_ = new MultiContainer().OfType<int>();
_ = new MultiContainer().OfType<long>();
_ = new MultiContainer().OfType<string>();
}
}");
}

[Fact]
public async Task NonGenericCasesCSharp()
{
Expand Down Expand Up @@ -406,6 +497,8 @@ void M()
[Fact]
public async Task NullableValueTypeCasesCSharp()
{
_ = Enumerable.Range(1, 5).Cast<int?>().ToArray();
_ = new int?[] { 123 }.Cast<int>().ToArray();
_ = new ValueType[] { StringComparison.OrdinalIgnoreCase, null }.Cast<StringComparison?>().ToArray();

await VerifyCS.VerifyAnalyzerAsync(@"
Expand All @@ -426,13 +519,56 @@ void M()
_ = (new object[0]).Cast<IntEnum?>();
_ = (new IntEnum?[0]).Cast<object>();
// nullable value types
_ = (new bool[0]).Cast<bool?>();
_ = (new bool?[0]).Cast<bool>();
_ = (new byte[0]).Cast<byte?>();
_ = (new byte?[0]).Cast<byte>();
_ = (new short[0]).Cast<short?>();
_ = (new short?[0]).Cast<short>();
_ = Enumerable.Range(1, 5).Cast<int?>();
_ = (new int?[0]).Cast<int>();
_ = (new long[0]).Cast<long?>();
_ = (new long?[0]).Cast<long>();
_ = (new float[0]).Cast<float?>();
_ = (new float?[0]).Cast<float>();
_ = (new double[0]).Cast<double?>();
_ = (new double?[0]).Cast<double>();
// Nullable<T>
_ = (new bool[0]).Cast<bool?>();
_ = (new Nullable<bool>[0]).Cast<bool>();
_ = (new byte[0]).Cast<Nullable<byte>>();
_ = (new Nullable<byte>[0]).Cast<byte>();
_ = (new short[0]).Cast<Nullable<short>>();
_ = (new Nullable<short>[0]).Cast<short>();
_ = Enumerable.Range(1, 5).Cast<Nullable<int>>();
_ = (new Nullable<int>[0]).Cast<int>();
_ = (new long[0]).Cast<Nullable<long>>();
_ = (new Nullable<long>[0]).Cast<long>();
_ = (new float[0]).Cast<Nullable<float>>();
_ = (new Nullable<float>[0]).Cast<float>();
_ = (new double[0]).Cast<Nullable<double>>();
_ = (new Nullable<double>[0]).Cast<double>();
// nullable value types
_ = (new byte[0]).Cast<byte?>();
_ = (new byte?[0]).Cast<byte>();
_ = (new short[0]).Cast<short?>();
_ = (new short?[0]).Cast<short>();
_ = Enumerable.Range(1, 5).Cast<int?>();
_ = (new int?[0]).Cast<int>();
_ = (new long[0]).Cast<long?>();
_ = (new long?[0]).Cast<long>();
// base class
_ = (new Enum[0]).Cast<IntEnum?>();
_ = (new IntEnum?[0]).Cast<Enum>();
_ = {|#10:(new int?[0]).Cast<Enum>()|};
_ = {|#11:(new Enum[0]).Cast<int?>()|};
// value type
// System.ValueType
_ = (new Enum[0]).Cast<ValueType>();
_ = Array.Empty<ValueType>().Cast<IntEnum?>();
_ = (new IntEnum?[0]).Cast<ValueType>();
Expand Down Expand Up @@ -494,9 +630,15 @@ void M()
[Fact]
public async Task GenericCastsCSharp()
{
await VerifyCS.VerifyAnalyzerAsync(@"
await new VerifyCS.Test
{
LanguageVersion = LanguageVersion.CSharp8,
TestCode = @"
using System;
using System.Linq;
#nullable enable
struct Struct : IInterface {}
interface IInterface {}
interface IInterface2 {}
Expand Down Expand Up @@ -598,13 +740,29 @@ void Mstruct<TStruct>() where TStruct : struct
{
(new TStruct[0]).Cast<int>(); // int is a struct
(new TStruct[0]).Cast<object>(); // can always cast to object
{|#50:(new TStruct[0]).Cast<string>()|}; // string is not is a struct
{|#50:(new TStruct[0]).Cast<string>()|}; // string is not struct
{|#51:(new TStruct[0]).Cast<string?>()|};
(new int[0]).Cast<TStruct>(); // int is a struct
(new object[0]).Cast<TStruct>(); // can always cast to object
{|#51:(new string[0]).Cast<TStruct>()|}; // string is not is a struct
(new object[0]).Cast<TStruct>(); // can always cast from object
{|#52:(new string[0]).Cast<TStruct>()|}; // string is not struct
{|#53:(new string?[0]).Cast<TStruct>()|};
(new Nullable<TStruct>[0]).Cast<TStruct>();
(new TStruct[0]).Cast<Nullable<TStruct>>();
(new Nullable<TStruct>[0]).Cast<int>(); // int is a struct
(new Nullable<TStruct>[0]).Cast<int?>();
(new Nullable<TStruct>[0]).Cast<object>(); // can always cast to object
(new Nullable<TStruct>[0]).Cast<object?>();
{|#54:(new Nullable<TStruct>[0]).Cast<string>()|}; // string is not struct
{|#55:(new Nullable<TStruct>[0]).Cast<string?>()|};
(new int[0]).Cast<TStruct?>(); // int is a struct
(new object[0]).Cast<TStruct?>(); // can always cast from object
{|#56:(new string[0]).Cast<TStruct?>()|}; // string is not struct
}
}",
ExpectedDiagnostics = {
VerifyCS.Diagnostic(castRule).WithLocation(10).WithArguments("TClassC", "string"),
VerifyCS.Diagnostic(castRule).WithLocation(11).WithArguments("string", "TClassC"),

Expand Down Expand Up @@ -633,8 +791,14 @@ void Mstruct<TStruct>() where TStruct : struct
VerifyCS.Diagnostic(castRule).WithLocation(41).WithArguments("int", "TStructInterface"),

VerifyCS.Diagnostic(castRule).WithLocation(50).WithArguments("TStruct", "string"),
VerifyCS.Diagnostic(castRule).WithLocation(51).WithArguments("string", "TStruct")
);
VerifyCS.Diagnostic(castRule).WithLocation(51).WithArguments("TStruct", "string?"),
VerifyCS.Diagnostic(castRule).WithLocation(52).WithArguments("string", "TStruct"),
VerifyCS.Diagnostic(castRule).WithLocation(53).WithArguments("string?", "TStruct"),
VerifyCS.Diagnostic(castRule).WithLocation(54).WithArguments("TStruct?", "string"),
VerifyCS.Diagnostic(castRule).WithLocation(55).WithArguments("TStruct?", "string?"),
VerifyCS.Diagnostic(castRule).WithLocation(56).WithArguments("string", "TStruct?"),
}
}.RunAsync();
}

[Fact]
Expand Down

0 comments on commit c6352bf

Please sign in to comment.