From 9f10355a3f079298421cb8d39e0d681c0ab2ca1f Mon Sep 17 00:00:00 2001 From: James May Date: Fri, 14 Feb 2025 11:43:12 +1100 Subject: [PATCH 1/3] fix false positive casting from value type constraint to Enum or ValueType #7031 --- ...stOrOfTypeWithIncompatibleTypesAnalyzer.cs | 3 +- ...fTypeWithIncompatibleTypesAnalyzerTests.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs index bc3dda1329..0542458402 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs @@ -267,7 +267,8 @@ static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbo if (castToTypeParam.HasValueTypeConstraint && castFrom.TypeKind == TypeKind.Class) { - return true; + return castFrom.SpecialType is not SpecialType.System_Enum + and not SpecialType.System_ValueType; } return false; diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs index 7e6523ce82..23337eb425 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs @@ -910,6 +910,35 @@ class GenericDerived : GenericBase await test.RunAsync(); } + [Fact, WorkItem(7031, "https://github.com/dotnet/roslyn-analyzers/issues/7031")] + public async Task GenericValueType() + { + // ensure runtime behavior is matches + _ = new Enum[] { StringComparison.OrdinalIgnoreCase }.Cast().ToArray(); + _ = new ValueType[] { int.MaxValue }.Cast().ToArray(); + + var test = new VerifyCS.Test + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net90, + LanguageVersion = LanguageVersion.Latest, + + TestCode = @" +using System; +using System.Collections.Generic; +using System.Linq; + +public static class Program +{ + public static IEnumerable CastEnums(IEnumerable values) where T : struct, Enum + => values.Cast(); // CA2021 + + public static IEnumerable CastValueTypes(IEnumerable values) where T : struct + => values.Cast(); // CA2021 +}" + }; + await test.RunAsync(); + } + [Fact] public async Task NonGenericCasesVB() { From ece5b33938048f2c5b08cde5da2a1ebc93499de3 Mon Sep 17 00:00:00 2001 From: James May Date: Mon, 17 Feb 2025 19:34:15 +1100 Subject: [PATCH 2/3] add inverse case --- ...ableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs | 3 ++- ...astOrOfTypeWithIncompatibleTypesAnalyzerTests.cs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs index 0542458402..081b91e7f5 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs @@ -248,7 +248,8 @@ static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbo if (castFromTypeParam.HasValueTypeConstraint && castTo.TypeKind == TypeKind.Class) { - return true; + return castTo.SpecialType is not SpecialType.System_Enum + and not SpecialType.System_ValueType; } return false; diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs index 23337eb425..cdab06eb0b 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs @@ -929,8 +929,17 @@ public async Task GenericValueType() public static class Program { - public static IEnumerable CastEnums(IEnumerable values) where T : struct, Enum - => values.Cast(); // CA2021 + public static IEnumerable CastFromEnums(IEnumerable values) where T : struct, Enum + => values.Cast(); + + public static IEnumerable CastFromValueTypes(IEnumerable values) where T : struct + => values.Cast(); + + public static IEnumerable CastToEnums(IEnumerable values) where T : struct, Enum + => values.Cast(); + + public static IEnumerable CastToValueTypes(IEnumerable values) where T : struct + => values.Cast(); public static IEnumerable CastValueTypes(IEnumerable values) where T : struct => values.Cast(); // CA2021 From a5b65ada3dcd7d90dee99c12d13d6687d35138d5 Mon Sep 17 00:00:00 2001 From: James May Date: Tue, 18 Feb 2025 16:38:18 +1100 Subject: [PATCH 3/3] add some more generic type contraint handling --- ...stOrOfTypeWithIncompatibleTypesAnalyzer.cs | 73 ++++++++++--------- ...fTypeWithIncompatibleTypesAnalyzerTests.cs | 44 ++++++++++- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs index 081b91e7f5..6770beb450 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer.cs @@ -216,16 +216,40 @@ static bool CastWillAlwaysFail(ITypeSymbol castFrom, ITypeSymbol castTo) return false; } - static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbol) - => !typeParameterSymbol.HasValueTypeConstraint - && typeParameterSymbol.ConstraintTypes.IsEmpty; - // 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 + 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) { @@ -235,44 +259,25 @@ static bool IsUnconstrainedTypeParameter(ITypeParameterSymbol typeParameterSymbo case (TypeKind.TypeParameter, _): var castFromTypeParam = (ITypeParameterSymbol)castFrom; - if (IsUnconstrainedTypeParameter(castFromTypeParam)) - { - return false; - } - if (castFromTypeParam.ConstraintTypes.Any(constraintType => CastWillAlwaysFail(constraintType, castTo))) { return true; } if (castFromTypeParam.HasValueTypeConstraint - && castTo.TypeKind == TypeKind.Class) - { - return castTo.SpecialType is not SpecialType.System_Enum - and not SpecialType.System_ValueType; - } - - return false; - case (_, TypeKind.TypeParameter): - var castToTypeParam = (ITypeParameterSymbol)castTo; - if (IsUnconstrainedTypeParameter(castToTypeParam)) - { - return false; - } - - if (castToTypeParam.ConstraintTypes.Any(constraintType => CastWillAlwaysFail(castFrom, constraintType))) + && ValueTypeConstraintImpossible(castTo)) { return true; } - if (castToTypeParam.HasValueTypeConstraint - && castFrom.TypeKind == TypeKind.Class) + if (castTo.TypeKind == TypeKind.TypeParameter) { - return castFrom.SpecialType is not SpecialType.System_Enum - and not SpecialType.System_ValueType; + return CastToTypeParamWillAlwaysFail(castFrom, (ITypeParameterSymbol)castTo); } return false; + case (_, TypeKind.TypeParameter): + return CastToTypeParamWillAlwaysFail(castFrom, (ITypeParameterSymbol)castTo); case (TypeKind.Class, TypeKind.Class): return !castFromParam.DerivesFrom(castToParam) diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs index cdab06eb0b..9ee0cd68ad 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzerTests.cs @@ -911,7 +911,7 @@ class GenericDerived : GenericBase } [Fact, WorkItem(7031, "https://github.com/dotnet/roslyn-analyzers/issues/7031")] - public async Task GenericValueType() + public async Task GenericConstraints() { // ensure runtime behavior is matches _ = new Enum[] { StringComparison.OrdinalIgnoreCase }.Cast().ToArray(); @@ -942,8 +942,46 @@ public static IEnumerable CastToValueTypes(IEnumerable values) => values.Cast(); public static IEnumerable CastValueTypes(IEnumerable values) where T : struct - => values.Cast(); // CA2021 -}" + => values.Cast(); + + public static IEnumerable CastUnconstrainedGeneric(IEnumerable values) + => values.Cast(); + + public static IEnumerable CastGenericUnmanagedToGeneric(IEnumerable values) + where TOut : unmanaged + => values.Cast(); + + public static IEnumerable CastGenericUnmanagedToGenericUnmanagedl(IEnumerable values) + where TIn : unmanaged + where TOut : unmanaged + => values.Cast(); + + public static IEnumerable CastGenericNotNullToGenericNotNull(IEnumerable values) + where TIn : notnull + where TOut : notnull + => values.Cast(); + + public static IEnumerable CastGenericToGeneric(IEnumerable values) + where TIn : Enum + where TOut : Uri + => {|#1:values.Cast()|}; + + public static IEnumerable CastGenericStructToGeneric(IEnumerable values) + where TIn : struct + where TOut : Uri + => {|#2:values.Cast()|}; + + public static IEnumerable CastGenericToGenericStruct(IEnumerable values) + where TIn : Uri + where TOut : struct + => {|#3:values.Cast()|}; +}", + ExpectedDiagnostics = + { + VerifyCS.Diagnostic(castRule).WithLocation(1).WithArguments("TIn", "TOut"), + VerifyCS.Diagnostic(castRule).WithLocation(2).WithArguments("TIn", "TOut"), + VerifyCS.Diagnostic(castRule).WithLocation(3).WithArguments("TIn", "TOut"), + } }; await test.RunAsync(); }