Skip to content

Commit 48887fe

Browse files
authored
Merge pull request #858 from nunit/Issue845_NUnit4002
Rewrite UseSpecificConstraintAnalyzer as an IOperation analyzer
2 parents 4cee336 + 0f7620c commit 48887fe

File tree

6 files changed

+142
-72
lines changed

6 files changed

+142
-72
lines changed

src/nunit.analyzers.tests/UseSpecificConstraint/UseSpecificConstraintAnalyzerTests.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,72 @@ public class UseSpecificConstraintAnalyzerTests
2828
#if NUNIT4
2929
["default(int)", "Default"],
3030
#endif
31+
["(byte)0", "Zero"],
32+
["(char)0", "Zero"],
33+
["(short)0", "Zero"],
34+
["(ushort)0", "Zero"],
35+
["0", "Zero"],
36+
["0.0", "Zero"],
37+
["0D", "Zero"],
38+
["0F", "Zero"],
39+
["0L", "Zero"],
40+
["0M", "Zero"],
41+
["0U", "Zero"],
42+
["0UL", "Zero"],
3143
];
3244

3345
[TestCaseSource(nameof(EqualToSpecificConstraint))]
3446
public void AnalyzeForSpecificConstraint(string literal, string constraint) => AnalyzeForEqualTo(literal, constraint);
3547

3648
#if NUNIT4
49+
/*
50+
* Is.EqualTo(default) no longer compiles with NUnit 4.3
51+
* As 'default' is untyped, the call is ambigous between the specificy typed overloads.
52+
*
53+
[Test]
54+
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
55+
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
56+
*/
57+
3758
[Test]
38-
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
39-
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
59+
public void AnalyzeForObjectAndSpecificTypeShouldNotSuggestIsDefault()
60+
{
61+
var testCode = TestUtility.WrapInTestMethod("""
62+
object o = 0;
63+
Assert.That(o, Is.EqualTo(default(int)));
64+
""");
65+
66+
RoslynAssert.Valid(analyzer, testCode);
67+
}
68+
69+
[TestCase(0)]
70+
public void AnalyzeForDynamicAndSpecificTypeShouldNotSuggestIsDefault(dynamic d)
71+
{
72+
// The below works, but the system determines the call at runtime.
73+
// When analyzing IInvocation we never see the 'Assert.That'
74+
// There is no overload for 'dynamic', so the system determines the call
75+
// depending on the actual type stored in the dynamic variable at runtime.
76+
Assert.That(d, Is.EqualTo(default(int)));
77+
Assert.That(d, Is.Default);
78+
79+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings("""
80+
[TestCase(0)]
81+
public void TestMethod(dynamic d)
82+
{
83+
Assert.That(d, Is.EqualTo(default(int)));
84+
}
85+
""");
86+
87+
RoslynAssert.Valid(analyzer, testCode);
88+
}
4089
#endif
4190

4291
private static void AnalyzeForEqualTo(string literal, string constraint, Settings? settings = null)
4392
{
4493
AnalyzeForEqualTo("Is", string.Empty, literal, constraint, settings);
4594
AnalyzeForEqualTo("Is", ".And.Not.Empty", literal, constraint, settings);
4695
AnalyzeForEqualTo("Is.Not", string.Empty, literal, constraint, settings);
47-
AnalyzeForEqualTo("Is.EqualTo(0).Or", string.Empty, literal, constraint, settings);
96+
AnalyzeForEqualTo("Is.EqualTo(4).Or", string.Empty, literal, constraint, settings);
4897
}
4998

5099
private static void AnalyzeForEqualTo(string prefix, string suffix, string literal, string constraint, Settings? settings = null)

src/nunit.analyzers.tests/UseSpecificConstraint/UseSpecificConstraintCodeFixTests.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,30 @@ public class UseSpecificConstraintCodeFixTests
3030
#if NUNIT4
3131
["default(int)", "Default"],
3232
#endif
33+
["0", "Zero"],
34+
["0.0", "Zero"],
3335
];
3436

3537
[TestCaseSource(nameof(EqualToSpecificConstraint))]
3638
public void AnalyzeForSpecificConstraint(string literal, string constraint) => AnalyzeForEqualTo(literal, constraint);
3739

3840
#if NUNIT4
39-
[Test]
40-
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
41-
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
41+
/*
42+
* Is.EqualTo(default) no longer compiles with NUnit 4.3
43+
* As 'default' is untyped, the call is ambigous between the specificy typed overloads.
44+
*
45+
[Test]
46+
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
47+
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
48+
*/
4249
#endif
4350

4451
private static void AnalyzeForEqualTo(string literal, string constraint, Settings? settings = null)
4552
{
4653
AnalyzeForEqualTo("Is", string.Empty, literal, constraint, settings);
4754
AnalyzeForEqualTo("Is", ".And.Not.Empty", literal, constraint, settings);
4855
AnalyzeForEqualTo("Is.Not", string.Empty, literal, constraint, settings);
49-
AnalyzeForEqualTo("Is.EqualTo(0).Or", string.Empty, literal, constraint, settings);
56+
AnalyzeForEqualTo("Is.EqualTo(4).Or", string.Empty, literal, constraint, settings);
5057
}
5158

5259
private static void AnalyzeForEqualTo(string prefix, string suffix, string literal, string constraint, Settings? settings = null)

src/nunit.analyzers/ComparableTypes/ComparableTypesAnalyzer.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected override void AnalyzeAssertInvocation(OperationAnalysisContext context
5858
foreach (var constraintPartExpression in constraintExpression.ConstraintParts)
5959
{
6060
if (constraintPartExpression.HasIncompatiblePrefixes()
61-
|| HasCustomComparer(constraintPartExpression)
61+
|| constraintPartExpression.HasCustomComparer()
6262
|| constraintPartExpression.HasUnknownExpressions())
6363
{
6464
continue;
@@ -169,10 +169,5 @@ private static bool IsIComparable(ITypeSymbol typeSymbol)
169169
return (typeSymbol.TypeKind == TypeKind.Interface && typeSymbol.GetFullMetadataName() == iComparable) ||
170170
typeSymbol.AllInterfaces.Any(i => i.TypeArguments.Length == 0 && i.GetFullMetadataName() == iComparable);
171171
}
172-
173-
private static bool HasCustomComparer(ConstraintExpressionPart constraintPartExpression)
174-
{
175-
return constraintPartExpression.GetSuffixesNames().Any(s => s == NUnitFrameworkConstants.NameOfEqualConstraintUsing);
176-
}
177172
}
178173
}

src/nunit.analyzers/EqualToIncompatibleTypes/EqualToIncompatibleTypesAnalyzer.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Immutable;
3-
using System.Linq;
43
using Microsoft.CodeAnalysis;
54
using Microsoft.CodeAnalysis.Diagnostics;
65
using Microsoft.CodeAnalysis.Operations;
@@ -50,7 +49,7 @@ protected override void AnalyzeAssertInvocation(OperationAnalysisContext context
5049
foreach (var constraintPartExpression in constraintExpression.ConstraintParts)
5150
{
5251
if (constraintPartExpression.HasIncompatiblePrefixes()
53-
|| HasCustomEqualityComparer(constraintPartExpression)
52+
|| constraintPartExpression.HasCustomComparer()
5453
|| constraintPartExpression.HasUnknownExpressions())
5554
{
5655
return;
@@ -141,10 +140,5 @@ private static void CheckActualVsExpectedOperation(OperationAnalysisContext cont
141140
expectedOperation.Syntax.GetLocation()));
142141
}
143142
}
144-
145-
private static bool HasCustomEqualityComparer(ConstraintExpressionPart constraintPartExpression)
146-
{
147-
return constraintPartExpression.GetSuffixesNames().Any(s => s == NUnitFrameworkConstants.NameOfEqualConstraintUsing);
148-
}
149143
}
150144
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Linq;
2+
using NUnit.Analyzers.Constants;
3+
4+
namespace NUnit.Analyzers.Operations
5+
{
6+
internal static class ConstraintExpressionPartExtensions
7+
{
8+
public static bool HasCustomComparer(this ConstraintExpressionPart constraintPartExpression)
9+
{
10+
return constraintPartExpression.GetSuffixesNames().Any(s => s == NUnitFrameworkConstants.NameOfEqualConstraintUsing);
11+
}
12+
}
13+
}
Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4-
using System.Linq;
54
using Microsoft.CodeAnalysis;
6-
using Microsoft.CodeAnalysis.CSharp;
7-
using Microsoft.CodeAnalysis.CSharp.Syntax;
85
using Microsoft.CodeAnalysis.Diagnostics;
6+
using Microsoft.CodeAnalysis.Operations;
97
using NUnit.Analyzers.Constants;
8+
using NUnit.Analyzers.Helpers;
9+
using NUnit.Analyzers.Operations;
1010

1111
namespace NUnit.Analyzers.UseSpecificConstraint
1212
{
1313
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14-
public sealed class UseSpecificConstraintAnalyzer : DiagnosticAnalyzer
14+
public sealed class UseSpecificConstraintAnalyzer : BaseAssertionAnalyzer
1515
{
1616
private static readonly DiagnosticDescriptor simplifyConstraint = DiagnosticDescriptorCreator.Create(
1717
id: AnalyzerIdentifiers.UseSpecificConstraint,
@@ -25,57 +25,84 @@ public sealed class UseSpecificConstraintAnalyzer : DiagnosticAnalyzer
2525
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
2626
ImmutableArray.Create(simplifyConstraint);
2727

28-
public override void Initialize(AnalysisContext context)
28+
protected override void AnalyzeAssertInvocation(Version nunitVersion, OperationAnalysisContext context, IInvocationOperation assertOperation)
2929
{
30-
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
31-
context.EnableConcurrentExecution();
32-
context.RegisterCompilationStartAction(this.AnalyzeCompilationStart);
33-
}
30+
if (!AssertHelper.TryGetActualAndConstraintOperations(assertOperation,
31+
out var actualOperation, out var constraintExpression))
32+
{
33+
return;
34+
}
3435

35-
private static void AnalyzeInvocation(Version nunitVersion, SyntaxNodeAnalysisContext context)
36-
{
37-
var invocationExpression = (InvocationExpressionSyntax)context.Node;
36+
var actualType = AssertHelper.GetUnwrappedActualType(actualOperation);
37+
if (actualType is null)
38+
return;
3839

39-
if (invocationExpression.ArgumentList.Arguments.Count == 1 &&
40-
invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression &&
41-
memberAccessExpression.Name.Identifier.Text == NUnitFrameworkConstants.NameOfIsEqualTo)
40+
foreach (var constraintPartExpression in constraintExpression.ConstraintParts)
4241
{
43-
ExpressionSyntax argument = invocationExpression.ArgumentList.Arguments[0].Expression;
42+
if (constraintPartExpression.HasIncompatiblePrefixes()
43+
|| constraintPartExpression.HasCustomComparer()
44+
|| constraintPartExpression.HasUnknownExpressions())
45+
{
46+
return;
47+
}
48+
49+
var constraintMethod = constraintPartExpression.GetConstraintMethod();
50+
if (constraintMethod?.Name != NUnitFrameworkConstants.NameOfIsEqualTo)
51+
continue;
52+
53+
var expectedOperation = constraintPartExpression.GetExpectedArgument();
54+
if (expectedOperation is null)
55+
continue;
56+
4457
string? constraint = null;
4558

46-
if (argument is LiteralExpressionSyntax literalExpression)
47-
{
48-
constraint = literalExpression.Kind() switch
49-
{
50-
SyntaxKind.NullLiteralExpression => NUnitFrameworkConstants.NameOfIsNull,
51-
SyntaxKind.FalseLiteralExpression => NUnitFrameworkConstants.NameOfIsFalse,
52-
SyntaxKind.TrueLiteralExpression => NUnitFrameworkConstants.NameOfIsTrue,
53-
_ => null,
54-
};
59+
// Look for both direct `0` and cast `(short)0` to catch all 0 values.
60+
ILiteralOperation? literalOperation = (expectedOperation is IConversionOperation conversionOperation ?
61+
conversionOperation.Operand : expectedOperation) as ILiteralOperation;
5562

56-
if (constraint is null && nunitVersion.Major >= 4)
63+
if (literalOperation is not null)
64+
{
65+
if (literalOperation.ConstantValue.HasValue)
5766
{
58-
constraint = literalExpression.Kind() switch
67+
constraint = literalOperation.ConstantValue.Value switch
5968
{
60-
SyntaxKind.DefaultLiteralExpression => NUnitV4FrameworkConstants.NameOfIsDefault,
61-
_ => constraint,
69+
null => NUnitFrameworkConstants.NameOfIsNull,
70+
false => NUnitFrameworkConstants.NameOfIsFalse,
71+
true => NUnitFrameworkConstants.NameOfIsTrue,
72+
0 or 0d or 0f or 0m => NUnitFrameworkConstants.NameOfIsZero,
73+
_ => null,
6274
};
75+
76+
if (constraint is null &&
77+
literalOperation.ConstantValue.Value is IConvertible convertible)
78+
{
79+
if (convertible.ToDouble(null) == 0)
80+
{
81+
// Catches all other 0 values: (byte)0, (short)0, 0u, 0L, 0uL
82+
constraint = NUnitFrameworkConstants.NameOfIsZero;
83+
}
84+
}
6385
}
6486
}
65-
else if (argument is DefaultExpressionSyntax defaultExpression)
87+
else if (expectedOperation is IDefaultValueOperation defaultValueOperation)
6688
{
67-
if (defaultExpression.Type is PredefinedTypeSyntax predefinedType)
89+
if (defaultValueOperation.Type is INamedTypeSymbol defaultType)
6890
{
69-
if (predefinedType.Keyword.IsKind(SyntaxKind.ObjectKeyword) ||
70-
predefinedType.Keyword.IsKind(SyntaxKind.StringKeyword))
91+
if (defaultType.SpecialType == SpecialType.System_Object ||
92+
defaultType.SpecialType == SpecialType.System_String)
7193
{
7294
constraint = NUnitFrameworkConstants.NameOfIsNull;
7395
}
7496
else if (nunitVersion.Major >= 4)
7597
{
98+
// We cannot use `Is.Default` if the actual type is `object`.
99+
// Note that the case `default(object)` is handled above.
100+
if (actualType.SpecialType == SpecialType.System_Object)
101+
continue;
102+
76103
constraint = NUnitV4FrameworkConstants.NameOfIsDefault;
77104
}
78-
else if (predefinedType.Keyword.IsKind(SyntaxKind.BoolKeyword))
105+
else if (defaultType.SpecialType == SpecialType.System_Boolean)
79106
{
80107
constraint = NUnitFrameworkConstants.NameOfIsFalse;
81108
}
@@ -84,31 +111,16 @@ invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpr
84111

85112
if (constraint is not null)
86113
{
87-
var diagnostic = Diagnostic.Create(simplifyConstraint, invocationExpression.GetLocation(),
114+
SyntaxNode syntax = constraintPartExpression.Root!.Syntax;
115+
var diagnostic = Diagnostic.Create(simplifyConstraint, syntax.GetLocation(),
88116
new Dictionary<string, string?>
89117
{
90118
[AnalyzerPropertyKeys.SpecificConstraint] = constraint,
91119
}.ToImmutableDictionary(),
92-
argument.ToString(), constraint);
120+
expectedOperation.Syntax.ToString(), constraint);
93121
context.ReportDiagnostic(diagnostic);
94122
}
95123
}
96124
}
97-
98-
private void AnalyzeCompilationStart(CompilationStartAnalysisContext context)
99-
{
100-
IEnumerable<AssemblyIdentity> referencedAssemblies = context.Compilation.ReferencedAssemblyNames;
101-
102-
AssemblyIdentity? nunit = referencedAssemblies.FirstOrDefault(a =>
103-
a.Name.Equals(NUnitFrameworkConstants.NUnitFrameworkAssemblyName, StringComparison.OrdinalIgnoreCase));
104-
105-
if (nunit is null)
106-
{
107-
// Who would use NUnit.Analyzers without NUnit?
108-
return;
109-
}
110-
111-
context.RegisterSyntaxNodeAction((context) => AnalyzeInvocation(nunit.Version, context), SyntaxKind.InvocationExpression);
112-
}
113125
}
114126
}

0 commit comments

Comments
 (0)