diff --git a/src/nunit.analyzers.tests/ValuesUsage/ValuesUsageAnalyzerTests.cs b/src/nunit.analyzers.tests/ValuesUsage/ValuesUsageAnalyzerTests.cs index 071253ce..0e8d5326 100644 --- a/src/nunit.analyzers.tests/ValuesUsage/ValuesUsageAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/ValuesUsage/ValuesUsageAnalyzerTests.cs @@ -158,6 +158,87 @@ public void Test([Values(TestEnum.A, TestEnum.B)] int e) { } RoslynAssert.Valid(this.analyzer, testCode); } + [Test] + public void AnalyzeWhenArgumentTypeGeneric() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + public sealed class AnalyzeWhenArgumentTypeIsCorrect + { + [Test] + public void ATest([Values(5, 5.0)] T blah) { } + }"); + RoslynAssert.Valid(this.analyzer, testCode); + } + + [TestCase("notnull")] + [TestCase("class")] + public void AnalyzeWhenArgumentTypeGenericWithConstraintsAndNonCompatible(string constraint) + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + public sealed class AnalyzeWhenArgumentTypeIsNotCorrect + {{ + [Test] + public void ATest([Values(↓null, ""5.0"")] T blah) where T : {constraint} {{ }} + }}"); + RoslynAssert.Diagnostics(this.analyzer, + ExpectedDiagnostic.Create(AnalyzerIdentifiers.ValuesParameterTypeMismatchUsage), + testCode); + } + + [TestCase("null")] + [TestCase("\"5.0\"")] + public void AnalyzeWhenArgumentTypeGenericWithStructConstraintsAndNonCompatible(string value) + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + public sealed class AnalyzeWhenArgumentTypeIsNotCorrect + {{ + [Test] + public void ATest([Values(↓{value})] T blah) where T : struct {{ }} + }}"); + RoslynAssert.Diagnostics(this.analyzer, + ExpectedDiagnostic.Create(AnalyzerIdentifiers.ValuesParameterTypeMismatchUsage), + testCode); + } + + [TestCase("", "class?")] + [TestCase("?", "class")] + public void AnalyzeWhenArgumentTypeGenericWithConstraintsAndCompatible(string nullableAnnotation, string constraint) + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing($@" + public sealed class AnalyzeWhenArgumentTypeIsNotCorrect + {{ + [Test] + public void ATest([Values(null, ""5.0"")] T{nullableAnnotation} blah) where T : {constraint} {{ }} + }}"); + RoslynAssert.Valid(this.analyzer, testCode); + } + + [Test] + public void AnalyzeWhenArgumentTypeGenericAndDifferentTypes() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + public sealed class AnalyzeWhenArgumentTypeIsNotCorrect + { + [Test] + public void ATest([Values(5, ↓""5.0"")] T blah) { } + }"); + RoslynAssert.Diagnostics(this.analyzer, + ExpectedDiagnostic.Create(AnalyzerIdentifiers.ValuesParameterTypeMismatchUsage), + testCode); + } + + [Test] + public void AnalyzeWhenArgumentTypeGenericResolvesNullableType() + { + var testCode = TestUtility.WrapClassInNamespaceAndAddUsing(@" + public sealed class AnalyzeWhenArgumentTypeIsNotCorrect + { + [Test] + public void ATest([Values(""5.0"", null)] T blah) { } + }"); + RoslynAssert.Valid(this.analyzer, testCode); + } + [Test] public void AnalyzeParameterIsArray() { diff --git a/src/nunit.analyzers/Extensions/AttributeArgumentTypedConstantExtensions.cs b/src/nunit.analyzers/Extensions/AttributeArgumentTypedConstantExtensions.cs index 26e6e9e8..57f147be 100644 --- a/src/nunit.analyzers/Extensions/AttributeArgumentTypedConstantExtensions.cs +++ b/src/nunit.analyzers/Extensions/AttributeArgumentTypedConstantExtensions.cs @@ -73,6 +73,8 @@ internal static bool CanAssignTo(this TypedConstant @this, ITypeSymbol target, C if (targetType is null) return false; + var typeParameter = targetType as ITypeParameterSymbol; + if (argumentValue is null) { if ( @@ -85,6 +87,21 @@ internal static bool CanAssignTo(this TypedConstant @this, ITypeSymbol target, C { return true; } + +#if !NETSTANDARD1_6 + if (typeParameter is not null) + { + if (typeParameter.HasValueTypeConstraint || + typeParameter.HasNotNullConstraint || + (typeParameter.HasReferenceTypeConstraint && typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.NotAnnotated)) + { + return false; + } + + // Either no constraint or class? + return true; + } +#endif } else { @@ -94,6 +111,17 @@ internal static bool CanAssignTo(this TypedConstant @this, ITypeSymbol target, C if (argumentType is null) return false; + if (typeParameter is not null) + { + if ((typeParameter.HasReferenceTypeConstraint && !argumentType.IsReferenceType) || + (typeParameter.HasValueTypeConstraint && argumentType.IsReferenceType)) + { + return false; + } + + return true; + } + if (targetType.IsAssignableFrom(argumentType) || (allowImplicitConversion && HasBuiltInImplicitConversion(argumentType, targetType, compilation))) { diff --git a/src/nunit.analyzers/ValuesUsage/ValuesUsageAnalyzer.cs b/src/nunit.analyzers/ValuesUsage/ValuesUsageAnalyzer.cs index cad99524..09d98a43 100644 --- a/src/nunit.analyzers/ValuesUsage/ValuesUsageAnalyzer.cs +++ b/src/nunit.analyzers/ValuesUsage/ValuesUsageAnalyzer.cs @@ -54,6 +54,8 @@ private static void AnalyzeParameter(SymbolAnalysisContext symbolContext, INamed var attributePositionalArguments = attribute.ConstructorArguments.AdjustArguments(); + TypedConstant? resolvedGeneric = null; + for (var index = 0; index < attributePositionalArguments.Length; index++) { var constructorArgument = attributePositionalArguments[index]; @@ -74,14 +76,48 @@ private static void AnalyzeParameter(SymbolAnalysisContext symbolContext, INamed continue; } + bool usesNullForgivessOperator = argumentSyntax.IsSuppressNullableWarning(); + var argumentTypeMatchesParameterType = constructorArgument.CanAssignTo(parameterSymbol.Type, symbolContext.Compilation, allowImplicitConversion: true, allowEnumToUnderlyingTypeConversion: true, - suppressNullableWarning: argumentSyntax.IsSuppressNullableWarning()); + suppressNullableWarning: usesNullForgivessOperator); if (argumentTypeMatchesParameterType) { - continue; + if (parameterSymbol.Type.TypeKind == TypeKind.TypeParameter) + { + if (resolvedGeneric is null) + { + // Remember first non-null argument to compare others to. + if (constructorArgument.Type is not null) + resolvedGeneric = constructorArgument; + + continue; + } + else + { + // The arguments must also be compatible with first matched class + // In case the first one is 'int' and the next one 'double' check reverse match as well + if (constructorArgument.CanAssignTo(resolvedGeneric.Value.Type!, + symbolContext.Compilation, + allowImplicitConversion: true, + allowEnumToUnderlyingTypeConversion: true, + suppressNullableWarning: usesNullForgivessOperator) || + resolvedGeneric.Value.CanAssignTo(constructorArgument.Type!, + symbolContext.Compilation, + allowImplicitConversion: true, + allowEnumToUnderlyingTypeConversion: true, + suppressNullableWarning: false /* resolvedGeneric has non-null value */)) + { + continue; + } + } + } + else + { + continue; + } } var diagnostic = Diagnostic.Create(parameterTypeMismatch,