Skip to content

Commit

Permalink
Check Values against generic type constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
manfred-brands committed Nov 14, 2023
1 parent 5a5415f commit 5c5ac1e
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 1 deletion.
55 changes: 55 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,61 @@ 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("struct")]
[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("", "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 AnalyzeWhenArgumentTypeGenericAndNonCompatible()
{
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 AnalyzeParameterIsArray()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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 @@ -84,6 +86,22 @@ internal static bool CanAssignTo(this TypedConstant @this, ITypeSymbol target, C
{
return true;
}

#if !NETSTANDARD1_6
if (typeParameter is not null)
{
if (typeParameter.HasReferenceTypeConstraint &&
typeParameter.ReferenceTypeConstraintNullableAnnotation != NullableAnnotation.NotAnnotated)
{
return true;
}

if (typeParameter.HasValueTypeConstraint || typeParameter.HasNotNullConstraint)
{
return false;
}
}
#endif
}
else
{
Expand All @@ -93,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
33 changes: 32 additions & 1 deletion 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 @@ -71,7 +73,36 @@ private static void AnalyzeParameter(SymbolAnalysisContext symbolContext, INamed
allowEnumToUnderlyingTypeConversion: true);
if (argumentTypeMatchesParameterType)
{
continue;
if (parameterSymbol.Type.TypeKind == TypeKind.TypeParameter)
{
if (resolvedGeneric is null)
{
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) ||
resolvedGeneric.Value.CanAssignTo(constructorArgument.Type!,
symbolContext.Compilation,
allowImplicitConversion: true,
allowEnumToUnderlyingTypeConversion: true))
{
continue;
}
}
}
else
{
continue;
}
}

var argumentSyntax = attribute.GetAdjustedArgumentSyntax(index,
Expand Down

0 comments on commit 5c5ac1e

Please sign in to comment.