Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Values against generic type constraints #638

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/nunit.analyzers.tests/ValuesUsage/ValuesUsageAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>([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<T>([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<T>([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<T>([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<T>([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<T>([Values(""5.0"", null)] T blah) { }
}");
RoslynAssert.Valid(this.analyzer, testCode);
}

[Test]
public void AnalyzeParameterIsArray()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
{
Expand All @@ -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)))
{
Expand Down
40 changes: 38 additions & 2 deletions src/nunit.analyzers/ValuesUsage/ValuesUsageAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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,
Expand Down