Skip to content

Commit ac3a370

Browse files
Add rule to convert EqualTo(xxx) into Is.XXX
This is for: Is.EqualTo(false) => Is.False Is.EqualTo(true) => Is.True Is.EqualTo(null) => Is.Null Is.EqualTo(default) => Is.Default Apply suggestions from code review Co-authored-by: Mikkel Nylander Bundgaard <[email protected]>
1 parent 60d45bf commit ac3a370

File tree

10 files changed

+420
-2
lines changed

10 files changed

+420
-2
lines changed

documentation/NUnit4002.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# NUnit4002
2+
3+
## Use Specific constraint
4+
5+
| Topic | Value
6+
| :-- | :--
7+
| Id | NUnit4002
8+
| Severity | Info
9+
| Enabled | True
10+
| Category | Style
11+
| Code | [UseSpecificConstraintAnalyzer](https://github.com/nunit/nunit.analyzers/blob/master/src/nunit.analyzers/UseSpecificConstraint/UseSpecificConstraintAnalyzer.cs)
12+
13+
## Description
14+
15+
Replace 'EqualTo' with a keyword in the corresponding specific constraint.
16+
17+
## Motivation
18+
19+
Sometimes constraints can be written more concisely using the inbuilt constraints provided by NUnit -
20+
e.g. `Is.True` instead of `Is.EqualTo(true)`.
21+
22+
Also, from NUnit version 4.3.0 where new overloads of `Is.EqualTo` were introduced, it is sometimes
23+
not possible to uniquely determine `default` when provided as the expected value - e.g. in
24+
`Is.EqualTo(default)`. Again, in such cases, it is better to use the inbuilt constraint provided by NUnit.
25+
26+
Some examples of constraints that can be be simplified by using a more specific constraint can be seen below.
27+
28+
```csharp
29+
[Test]
30+
public void Test()
31+
{
32+
Assert.That(actualFalse, Is.EqualTo(false));
33+
Assert.That(actualTrue, Is.EqualTo(true));
34+
Assert.That(actualObject, Is.EqualTo(null));
35+
Assert.That(actualObject, Is.EqualTo(default));
36+
Assert.That(actualInt, Is.EqualTo(default));
37+
}
38+
39+
## How to fix violations
40+
41+
The analyzer comes with a code fix that will replace the constraint `Is.EqualTo(x)` with
42+
the matching `Is.X` constraint (for some `x`). So the code block above will be changed into
43+
44+
```csharp
45+
[Test]
46+
public void Test()
47+
{
48+
Assert.That(actualFalse, Is.False);
49+
Assert.That(actualTrue, Is.True);
50+
Assert.That(actualObject, Is.Null);
51+
Assert.That(actualObject, Is.Null);
52+
Assert.That(actualInt, Is.Default);
53+
}
54+
55+
<!-- start generated config severity -->
56+
## Configure severity
57+
58+
### Via ruleset file
59+
60+
Configure the severity per project, for more info see
61+
[MSDN](https://learn.microsoft.com/en-us/visualstudio/code-quality/using-rule-sets-to-group-code-analysis-rules?view=vs-2022).
62+
63+
### Via .editorconfig file
64+
65+
```ini
66+
# NUnit4002: Use Specific constraint
67+
dotnet_diagnostic.NUnit4002.severity = chosenSeverity
68+
```
69+
70+
where `chosenSeverity` can be one of `none`, `silent`, `suggestion`, `warning`, or `error`.
71+
72+
### Via #pragma directive
73+
74+
```csharp
75+
#pragma warning disable NUnit4002 // Use Specific constraint
76+
Code violating the rule here
77+
#pragma warning restore NUnit4002 // Use Specific constraint
78+
```
79+
80+
Or put this at the top of the file to disable all instances.
81+
82+
```csharp
83+
#pragma warning disable NUnit4002 // Use Specific constraint
84+
```
85+
86+
### Via attribute `[SuppressMessage]`
87+
88+
```csharp
89+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style",
90+
"NUnit4002:Use Specific constraint",
91+
Justification = "Reason...")]
92+
```
93+
<!-- end generated config severity -->

documentation/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,5 @@ Rules which help you write concise and readable NUnit test code.
132132

133133
| Id | Title | :mag: | :memo: | :bulb: |
134134
| :-- | :-- | :--: | :--: | :--: |
135-
| [NUnit4001](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit4001.md) | Simplify the Values attribute | :white_check_mark: | :information_source: | :x: |
135+
| [NUnit4001](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit4001.md) | Simplify the Values attribute | :white_check_mark: | :information_source: | :white_check_mark: |
136+
| [NUnit4002](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit4002.md) | Use Specific constraint | :white_check_mark: | :information_source: | :white_check_mark: |
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using NUnit.Analyzers.Constants;
11+
12+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
13+
14+
namespace NUnit.Analyzers.UseSpecificConstraint
15+
{
16+
[ExportCodeFixProvider(LanguageNames.CSharp)]
17+
[Shared]
18+
internal class UseSpecificConstraintCodeFix : CodeFixProvider
19+
{
20+
internal const string UseSpecificConstraint = "Use specific constraint";
21+
22+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
23+
AnalyzerIdentifiers.UseSpecificConstraint);
24+
25+
public sealed override FixAllProvider GetFixAllProvider()
26+
{
27+
return WellKnownFixAllProviders.BatchFixer;
28+
}
29+
30+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
31+
{
32+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
33+
34+
if (root is null)
35+
{
36+
return;
37+
}
38+
39+
context.CancellationToken.ThrowIfCancellationRequested();
40+
41+
SyntaxNode node = root.FindNode(context.Span);
42+
43+
if (node is ArgumentSyntax argument)
44+
{
45+
node = argument.Expression;
46+
}
47+
48+
if (node is not InvocationExpressionSyntax originalExpression ||
49+
originalExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
50+
{
51+
return;
52+
}
53+
54+
var diagnostic = context.Diagnostics.First();
55+
var constraint = diagnostic.Properties[AnalyzerPropertyKeys.SpecificConstraint]!;
56+
57+
var newExpression = MemberAccessExpression(
58+
SyntaxKind.SimpleMemberAccessExpression,
59+
memberAccessExpression.Expression,
60+
IdentifierName(constraint));
61+
62+
SyntaxNode newRoot = root.ReplaceNode(originalExpression, newExpression);
63+
64+
var codeAction = CodeAction.Create(
65+
UseSpecificConstraint,
66+
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)),
67+
UseSpecificConstraint);
68+
69+
context.RegisterCodeFix(codeAction, context.Diagnostics);
70+
}
71+
}
72+
}

src/nunit.analyzers.tests/DocumentationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ string GetSection(string doc, string startToken, string endToken)
215215

216216
[TestCase(Categories.Structure, 0)]
217217
[TestCase(Categories.Assertion, 1)]
218-
[TestCase(Categories.Style, 2)]
218+
[TestCase(Categories.Style, 3)]
219219
public void EnsureThatAnalyzerIndexIsAsExpected(string category, int tableNumber)
220220
{
221221
var builder = new StringBuilder();
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using Gu.Roslyn.Asserts;
2+
using Microsoft.CodeAnalysis.Diagnostics;
3+
using NUnit.Analyzers.Constants;
4+
using NUnit.Analyzers.UseSpecificConstraint;
5+
using NUnit.Framework;
6+
7+
namespace NUnit.Analyzers.Tests.UseSpecificConstraint
8+
{
9+
public class UseSpecificConstraintAnalyzerTests
10+
{
11+
private static readonly DiagnosticAnalyzer analyzer = new UseSpecificConstraintAnalyzer();
12+
private static readonly ExpectedDiagnostic expectedDiagnostic =
13+
ExpectedDiagnostic.Create(AnalyzerIdentifiers.UseSpecificConstraint);
14+
15+
private static readonly string[][] EqualToSpecificConstraint =
16+
[
17+
["false", "False"],
18+
#if NUNIT4
19+
["default(bool)", "Default"],
20+
#else
21+
["default(bool)", "False"],
22+
#endif
23+
["true", "True"],
24+
25+
["null", "Null"],
26+
["default(object)", "Null"],
27+
["default(string)", "Null"],
28+
#if NUNIT4
29+
["default(int)", "Default"],
30+
#endif
31+
];
32+
33+
[TestCaseSource(nameof(EqualToSpecificConstraint))]
34+
public void AnalyzeForSpecificConstraint(string literal, string constraint) => AnalyzeForEqualTo(literal, constraint);
35+
36+
#if NUNIT4
37+
[Test]
38+
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
39+
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
40+
#endif
41+
42+
private static void AnalyzeForEqualTo(string literal, string constraint, Settings? settings = null)
43+
{
44+
AnalyzeForEqualTo("Is", string.Empty, literal, constraint, settings);
45+
AnalyzeForEqualTo("Is", ".And.Not.Empty", literal, constraint, settings);
46+
AnalyzeForEqualTo("Is.Not", string.Empty, literal, constraint, settings);
47+
AnalyzeForEqualTo("Is.EqualTo(0).Or", string.Empty, literal, constraint, settings);
48+
}
49+
50+
private static void AnalyzeForEqualTo(string prefix, string suffix, string literal, string constraint, Settings? settings = null)
51+
{
52+
var testCode = TestUtility.WrapInTestMethod(
53+
$"Assert.That(false, ↓{prefix}.EqualTo({literal}){suffix});");
54+
55+
RoslynAssert.Diagnostics(analyzer,
56+
expectedDiagnostic.WithMessage($"Replace 'Is.EqualTo({literal})' with 'Is.{constraint}' constraint"),
57+
testCode, settings);
58+
}
59+
}
60+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Gu.Roslyn.Asserts;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using NUnit.Analyzers.Constants;
5+
using NUnit.Analyzers.UseSpecificConstraint;
6+
using NUnit.Framework;
7+
8+
namespace NUnit.Analyzers.Tests.UseSpecificConstraint
9+
{
10+
public class UseSpecificConstraintCodeFixTests
11+
{
12+
private static readonly DiagnosticAnalyzer analyzer = new UseSpecificConstraintAnalyzer();
13+
private static readonly CodeFixProvider fix = new UseSpecificConstraintCodeFix();
14+
private static readonly ExpectedDiagnostic expectedDiagnostic =
15+
ExpectedDiagnostic.Create(AnalyzerIdentifiers.UseSpecificConstraint);
16+
17+
private static readonly string[][] EqualToSpecificConstraint =
18+
[
19+
["false", "False"],
20+
#if NUNIT4
21+
["default(bool)", "Default"],
22+
#else
23+
["default(bool)", "False"],
24+
#endif
25+
["true", "True"],
26+
27+
["null", "Null"],
28+
["default(object)", "Null"],
29+
["default(string)", "Null"],
30+
#if NUNIT4
31+
["default(int)", "Default"],
32+
#endif
33+
];
34+
35+
[TestCaseSource(nameof(EqualToSpecificConstraint))]
36+
public void AnalyzeForSpecificConstraint(string literal, string constraint) => AnalyzeForEqualTo(literal, constraint);
37+
38+
#if NUNIT4
39+
[Test]
40+
public void AnalyzeForIsDefault() => AnalyzeForEqualTo("default", "Default",
41+
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));
42+
#endif
43+
44+
private static void AnalyzeForEqualTo(string literal, string constraint, Settings? settings = null)
45+
{
46+
AnalyzeForEqualTo("Is", string.Empty, literal, constraint, settings);
47+
AnalyzeForEqualTo("Is", ".And.Not.Empty", literal, constraint, settings);
48+
AnalyzeForEqualTo("Is.Not", string.Empty, literal, constraint, settings);
49+
AnalyzeForEqualTo("Is.EqualTo(0).Or", string.Empty, literal, constraint, settings);
50+
}
51+
52+
private static void AnalyzeForEqualTo(string prefix, string suffix, string literal, string constraint, Settings? settings = null)
53+
{
54+
var testCode = TestUtility.WrapInTestMethod(
55+
$"Assert.That(false, ↓{prefix}.EqualTo({literal}){suffix});");
56+
57+
var fixedCode = TestUtility.WrapInTestMethod(
58+
$"Assert.That(false, {prefix}.{constraint}{suffix});");
59+
60+
RoslynAssert.CodeFix(analyzer, fix,
61+
expectedDiagnostic.WithMessage($"Replace 'Is.EqualTo({literal})' with 'Is.{constraint}' constraint"),
62+
testCode, fixedCode,
63+
settings: settings);
64+
}
65+
}
66+
}

src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ internal static class AnalyzerIdentifiers
111111
#region Style
112112

113113
internal const string SimplifyValues = "NUnit4001";
114+
internal const string UseSpecificConstraint = "NUnit4002";
114115

115116
#endregion
116117
}

src/nunit.analyzers/Constants/AnalyzerPropertyKeys.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ internal static class AnalyzerPropertyKeys
77
internal const string MinimumNumberOfArguments = nameof(AnalyzerPropertyKeys.MinimumNumberOfArguments);
88

99
internal const string SupportsEnterMultipleScope = nameof(AnalyzerPropertyKeys.SupportsEnterMultipleScope);
10+
11+
internal const string SpecificConstraint = nameof(AnalyzerPropertyKeys.SpecificConstraint);
1012
}
1113
}

0 commit comments

Comments
 (0)