Skip to content

Commit b59c564

Browse files
Merge pull request #772 from manfred-brands/Issue770_TestContext.Write
Added TestContext.Write Is Obsolete Analyzer
2 parents a00035e + bf26b78 commit b59c564

12 files changed

+358
-0
lines changed

documentation/NUnit1033.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# NUnit1033
2+
3+
## The Write methods on TestContext will be marked as Obsolete and eventually removed
4+
5+
| Topic | Value
6+
| :-- | :--
7+
| Id | NUnit1033
8+
| Severity | Warning
9+
| Enabled | True
10+
| Category | Structure
11+
| Code | [TestContextWriteIsObsoleteAnalyzer](https://github.com/nunit/nunit.analyzers/blob/master/src/nunit.analyzers/TestContextWriteIsObsolete/TestContextWriteIsObsoleteAnalyzer.cs)
12+
13+
## Description
14+
15+
Direct `Write` calls should be replaced with `Out.Write`.
16+
17+
Future version of NUnit will first mark the `.Write` methods on `TestContext`
18+
as `Obsolete` and eventually remove them.
19+
20+
This rule allows updating your code before the methods are removed.
21+
22+
## Motivation
23+
24+
The `Write` methods are simple wrappers calling `Out.Write`.
25+
There is no wrapper for `Error` which always required to use `TestContext.Error.Write`.
26+
Besides this being inconsistent, later versions of .NET added new overloads,
27+
e.g. for `ReadOnlySpan<char>` and `async` methods like `WriteAsync`.
28+
Instead of adding more and more dummy wrappers, it was decided that user code should use
29+
the `Out` property and then can use any `Write` overload available on `TextWriter`.
30+
31+
## How to fix violations
32+
33+
Simply insert `.Out` between `TestContext` and `.Write`.
34+
35+
`TestContext.WriteLine("This isn't right");`
36+
37+
becomes
38+
39+
`TestContext.Out.WriteLine("This isn't right");`
40+
41+
<!-- start generated config severity -->
42+
## Configure severity
43+
44+
### Via ruleset file
45+
46+
Configure the severity per project, for more info see
47+
[MSDN](https://learn.microsoft.com/en-us/visualstudio/code-quality/using-rule-sets-to-group-code-analysis-rules?view=vs-2022).
48+
49+
### Via .editorconfig file
50+
51+
```ini
52+
# NUnit1033: The Write methods on TestContext will be marked as Obsolete and eventually removed
53+
dotnet_diagnostic.NUnit1033.severity = chosenSeverity
54+
```
55+
56+
where `chosenSeverity` can be one of `none`, `silent`, `suggestion`, `warning`, or `error`.
57+
58+
### Via #pragma directive
59+
60+
```csharp
61+
#pragma warning disable NUnit1033 // The Write methods on TestContext will be marked as Obsolete and eventually removed
62+
Code violating the rule here
63+
#pragma warning restore NUnit1033 // The Write methods on TestContext will be marked as Obsolete and eventually removed
64+
```
65+
66+
Or put this at the top of the file to disable all instances.
67+
68+
```csharp
69+
#pragma warning disable NUnit1033 // The Write methods on TestContext will be marked as Obsolete and eventually removed
70+
```
71+
72+
### Via attribute `[SuppressMessage]`
73+
74+
```csharp
75+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Structure",
76+
"NUnit1033:The Write methods on TestContext will be marked as Obsolete and eventually removed",
77+
Justification = "Reason...")]
78+
```
79+
<!-- end generated config severity -->

documentation/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Rules which enforce structural requirements on the test code.
5151
| [NUnit1030](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit1030.md) | The type of parameter provided by the TestCaseSource does not match the type of the parameter in the Test method | :white_check_mark: | :exclamation: | :x: |
5252
| [NUnit1031](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit1031.md) | The individual arguments provided by a ValuesAttribute must match the type of the corresponding parameter of the method | :white_check_mark: | :exclamation: | :x: |
5353
| [NUnit1032](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit1032.md) | An IDisposable field/property should be Disposed in a TearDown method | :white_check_mark: | :exclamation: | :x: |
54+
| [NUnit1033](https://github.com/nunit/nunit.analyzers/tree/master/documentation/NUnit1033.md) | The Write methods on TestContext will be marked as Obsolete and eventually removed | :white_check_mark: | :warning: | :white_check_mark: |
5455

5556
## Assertion Rules (NUnit2001 - )
5657

src/nunit.analyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "documentation", "documentat
4242
..\documentation\NUnit1030.md = ..\documentation\NUnit1030.md
4343
..\documentation\NUnit1031.md = ..\documentation\NUnit1031.md
4444
..\documentation\NUnit1032.md = ..\documentation\NUnit1032.md
45+
..\documentation\NUnit1033.md = ..\documentation\NUnit1033.md
4546
..\documentation\NUnit2001.md = ..\documentation\NUnit2001.md
4647
..\documentation\NUnit2002.md = ..\documentation\NUnit2002.md
4748
..\documentation\NUnit2003.md = ..\documentation\NUnit2003.md

src/nunit.analyzers.tests/Constants/NUnitFrameworkConstantsTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public sealed class NUnitFrameworkConstantsTests
7676
(nameof(NUnitFrameworkConstants.NameOfMultipleAsync), "MultipleAsync"),
7777
#endif
7878

79+
(nameof(NUnitFrameworkConstants.NameOfOut), nameof(TestContext.Out)),
80+
(nameof(NUnitFrameworkConstants.NameOfWrite), nameof(TestContext.Out.Write)),
81+
(nameof(NUnitFrameworkConstants.NameOfWriteLine), nameof(TestContext.Out.WriteLine)),
82+
7983
(nameof(NUnitFrameworkConstants.NameOfThrows), nameof(Throws)),
8084
(nameof(NUnitFrameworkConstants.NameOfThrowsArgumentException), nameof(Throws.ArgumentException)),
8185
(nameof(NUnitFrameworkConstants.NameOfThrowsArgumentNullException), nameof(Throws.ArgumentNullException)),
@@ -208,6 +212,8 @@ public sealed class NUnitFrameworkConstantsTests
208212
(nameof(NUnitFrameworkConstants.FullNameOfCancelAfterAttribute), typeof(CancelAfterAttribute)),
209213
(nameof(NUnitFrameworkConstants.FullNameOfCancellationToken), typeof(CancellationToken)),
210214

215+
(nameof(NUnitFrameworkConstants.FullNameOfTypeTestContext), typeof(TestContext)),
216+
211217
(nameof(NUnitFrameworkConstants.FullNameOfSameAsConstraint), typeof(SameAsConstraint)),
212218
(nameof(NUnitFrameworkConstants.FullNameOfSomeItemsConstraint), typeof(SomeItemsConstraint)),
213219
(nameof(NUnitFrameworkConstants.FullNameOfEqualToConstraint), typeof(EqualConstraint)),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Gu.Roslyn.Asserts;
2+
using Microsoft.CodeAnalysis.Diagnostics;
3+
using NUnit.Analyzers.Constants;
4+
using NUnit.Analyzers.TestContextWriteIsObsolete;
5+
using NUnit.Framework;
6+
7+
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete
8+
{
9+
public class TestContextWriteIsObsoleteAnalyzerTests
10+
{
11+
private static readonly DiagnosticAnalyzer analyzer = new TestContextWriteIsObsoleteAnalyzer();
12+
private static readonly ExpectedDiagnostic expectedDiagnostic =
13+
ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestContextWriteIsObsolete);
14+
15+
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))]
16+
public void AnyDirectWriteMethod(string writeMethodAndParameters)
17+
{
18+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
19+
public void Test()
20+
{{
21+
↓TestContext.{writeMethodAndParameters};
22+
}}");
23+
24+
RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode);
25+
}
26+
27+
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))]
28+
public void AnyIndirectWriteMethod(string writeMethodAndParameters)
29+
{
30+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
31+
public void Test()
32+
{{
33+
TestContext.Out.{writeMethodAndParameters};
34+
}}");
35+
36+
RoslynAssert.Valid(analyzer, testCode);
37+
}
38+
}
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Gu.Roslyn.Asserts;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using NUnit.Analyzers.Constants;
5+
using NUnit.Analyzers.TestContextWriteIsObsolete;
6+
using NUnit.Framework;
7+
8+
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete
9+
{
10+
public class TestContextWriteIsObsoleteCodeFixTests
11+
{
12+
private static readonly DiagnosticAnalyzer analyzer = new TestContextWriteIsObsoleteAnalyzer();
13+
private static readonly CodeFixProvider fix = new TestContextWriteIsObsoleteCodeFix();
14+
private static readonly ExpectedDiagnostic expectedDiagnostic =
15+
ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestContextWriteIsObsolete);
16+
17+
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))]
18+
public void AnyWriteMethod(string writeMethodAndParameters)
19+
{
20+
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
21+
public void Test()
22+
{{
23+
↓TestContext.{writeMethodAndParameters};
24+
}}");
25+
26+
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
27+
public void Test()
28+
{{
29+
TestContext.Out.{writeMethodAndParameters};
30+
}}");
31+
32+
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode,
33+
fixTitle: TestContextWriteIsObsoleteCodeFix.InsertOutDescription);
34+
}
35+
}
36+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete
2+
{
3+
internal static class TestContextWriteIsObsoleteTestCases
4+
{
5+
public static readonly string[] WriteInvocations =
6+
{
7+
"Write(true)",
8+
"Write('!')",
9+
"Write(new char[] { '!', '!' })",
10+
"Write(default(char[]))",
11+
"Write(1D)",
12+
"Write(1)",
13+
"Write(1L)",
14+
"Write(1M)",
15+
"Write(default(object))",
16+
"Write(1F)",
17+
"Write(\"NUnit\")",
18+
"Write(default(string))",
19+
"Write(1U)",
20+
"Write(1UL)",
21+
"Write(\"{0}\", 1)",
22+
"Write(\"{0} + {1}\", 1, 2)",
23+
"Write(\"{0} + {1} = {2}\", 1, 2, 3)",
24+
"Write(\"{0} + {1} = {2} + {3}\", 1, 2, 2, 1)",
25+
"WriteLine()",
26+
"WriteLine(true)",
27+
"WriteLine('!')",
28+
"WriteLine(new char[] { '!', '!' })",
29+
"WriteLine(default(char[]))",
30+
"WriteLine(1D)",
31+
"WriteLine(1)",
32+
"WriteLine(1L)",
33+
"WriteLine(1M)",
34+
"WriteLine(default(object))",
35+
"WriteLine(1F)",
36+
"WriteLine(\"NUnit\")",
37+
"Write(default(string))",
38+
"WriteLine(1U)",
39+
"WriteLine(1UL)",
40+
"WriteLine(\"{0}\", 1)",
41+
"WriteLine(\"{0} + {1}\", 1, 2)",
42+
"WriteLine(\"{0} + {1} = {2}\", 1, 2, 3)",
43+
"WriteLine(\"{0} + {1} = {2} + {3}\", 1, 2, 2, 1)",
44+
};
45+
}
46+
}

src/nunit.analyzers/Constants/AnalyzerIdentifiers.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal static class AnalyzerIdentifiers
3636
internal const string TestCaseSourceMismatchWithTestMethodParameterType = "NUnit1030";
3737
internal const string ValuesParameterTypeMismatchUsage = "NUnit1031";
3838
internal const string FieldIsNotDisposedInTearDown = "NUnit1032";
39+
internal const string TestContextWriteIsObsolete = "NUnit1033";
3940

4041
#endregion Structure
4142

src/nunit.analyzers/Constants/NUnitFrameworkConstants.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public static class NUnitFrameworkConstants
5252
public const string NameOfMultiple = "Multiple";
5353
public const string NameOfMultipleAsync = "MultipleAsync";
5454

55+
public const string NameOfOut = "Out";
56+
public const string NameOfWrite = "Write";
57+
public const string NameOfWriteLine = "WriteLine";
58+
5559
public const string NameOfThrows = "Throws";
5660
public const string NameOfThrowsArgumentException = "ArgumentException";
5761
public const string NameOfThrowsArgumentNullException = "ArgumentNullException";
@@ -153,6 +157,8 @@ public static class NUnitFrameworkConstants
153157
public const string FullNameOfCancelAfterAttribute = "NUnit.Framework.CancelAfterAttribute";
154158
public const string FullNameOfCancellationToken = "System.Threading.CancellationToken";
155159

160+
public const string FullNameOfTypeTestContext = "NUnit.Framework.TestContext";
161+
156162
public const string NameOfConstraint = "Constraint";
157163

158164
public const string FullNameOfSameAsConstraint = "NUnit.Framework.Constraints.SameAsConstraint";
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using Microsoft.CodeAnalysis.Operations;
5+
using NUnit.Analyzers.Constants;
6+
7+
namespace NUnit.Analyzers.TestContextWriteIsObsolete
8+
{
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class TestContextWriteIsObsoleteAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly DiagnosticDescriptor descriptor = DiagnosticDescriptorCreator.Create(
13+
id: AnalyzerIdentifiers.TestContextWriteIsObsolete,
14+
title: TestContextWriteIsObsoleteAnalyzerConstants.Title,
15+
messageFormat: TestContextWriteIsObsoleteAnalyzerConstants.Message,
16+
category: Categories.Structure,
17+
defaultSeverity: DiagnosticSeverity.Warning,
18+
description: TestContextWriteIsObsoleteAnalyzerConstants.Description);
19+
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(descriptor);
21+
22+
public override void Initialize(AnalysisContext context)
23+
{
24+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
25+
context.EnableConcurrentExecution();
26+
context.RegisterCompilationStartAction(AnalyzeCompilationStart);
27+
}
28+
29+
private static void AnalyzeCompilationStart(CompilationStartAnalysisContext context)
30+
{
31+
INamedTypeSymbol? testContextType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfTypeTestContext);
32+
if (testContextType is null)
33+
{
34+
return;
35+
}
36+
37+
context.RegisterOperationAction(context => AnalyzeInvocation(testContextType, context), OperationKind.Invocation);
38+
}
39+
40+
private static void AnalyzeInvocation(INamedTypeSymbol testContextType, OperationAnalysisContext context)
41+
{
42+
if (context.Operation is not IInvocationOperation invocationOperation)
43+
return;
44+
45+
// TestContext.Write methods are static methods
46+
if (invocationOperation.Instance is not null)
47+
return;
48+
49+
IMethodSymbol targetMethod = invocationOperation.TargetMethod;
50+
51+
if (!targetMethod.ReturnsVoid)
52+
return;
53+
54+
context.CancellationToken.ThrowIfCancellationRequested();
55+
56+
if (!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, testContextType))
57+
return;
58+
59+
if (targetMethod.Name is NUnitFrameworkConstants.NameOfWrite or
60+
NUnitFrameworkConstants.NameOfWriteLine)
61+
{
62+
context.ReportDiagnostic(Diagnostic.Create(
63+
descriptor,
64+
invocationOperation.Syntax.GetLocation()));
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)