-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added TestContext.Write Is Obsolete Analyzer
- Loading branch information
1 parent
5fde5a5
commit b1fa587
Showing
11 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# NUnit1033 | ||
|
||
## The Write methods on TestContext are Obsolete | ||
|
||
| Topic | Value | ||
| :-- | :-- | ||
| Id | NUnit1033 | ||
| Severity | Error | ||
| Enabled | True | ||
| Category | Structure | ||
| Code | [TestContextWriteIsObsoleteAnalyzer](https://github.com/nunit/nunit.analyzers/blob/master/src/nunit.analyzers/TestContextWriteIsObsolete/TestContextWriteIsObsoleteAnalyzer.cs) | ||
|
||
## Description | ||
|
||
Direct Write calls should be replaced with Out.Write. | ||
|
||
## Motivation | ||
|
||
The `Write` methods are simple wrappers calling `Out.Write`. There is no wrapper for `Error` which always required to use `TestContext.Error.Write`. | ||
Besides this being inconsistent, later versions of .NET added new overloads, e.g. for `ReadOnlySpan<char>` and `async` methods like `WriteAsync`. | ||
Instead of adding more and more dummy wrappers, it was decided that user code should use the `Out` property and then can use any `Write` overload available on `TextWriter`. | ||
|
||
## How to fix violations | ||
|
||
Simple insert `.Out` between `TestContext` and `.Write`. | ||
|
||
<!-- start generated config severity --> | ||
## Configure severity | ||
|
||
### Via ruleset file | ||
|
||
Configure the severity per project, for more info see | ||
[MSDN](https://learn.microsoft.com/en-us/visualstudio/code-quality/using-rule-sets-to-group-code-analysis-rules?view=vs-2022). | ||
|
||
### Via .editorconfig file | ||
|
||
```ini | ||
# NUnit1033: The Write methods on TestContext are Obsolete | ||
dotnet_diagnostic.NUnit1033.severity = chosenSeverity | ||
``` | ||
|
||
where `chosenSeverity` can be one of `none`, `silent`, `suggestion`, `warning`, or `error`. | ||
|
||
### Via #pragma directive | ||
|
||
```csharp | ||
#pragma warning disable NUnit1033 // The Write methods on TestContext are Obsolete | ||
Code violating the rule here | ||
#pragma warning restore NUnit1033 // The Write methods on TestContext are Obsolete | ||
``` | ||
|
||
Or put this at the top of the file to disable all instances. | ||
|
||
```csharp | ||
#pragma warning disable NUnit1033 // The Write methods on TestContext are Obsolete | ||
``` | ||
|
||
### Via attribute `[SuppressMessage]` | ||
|
||
```csharp | ||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Structure", | ||
"NUnit1033:The Write methods on TestContext are Obsolete", | ||
Justification = "Reason...")] | ||
``` | ||
<!-- end generated config severity --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
...nit.analyzers.tests/TestContextWriteIsObsolete/TestContextWriteIsObsoleteAnalyzerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using Gu.Roslyn.Asserts; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using NUnit.Analyzers.Constants; | ||
using NUnit.Analyzers.TestContextWriteIsObsolete; | ||
using NUnit.Framework; | ||
|
||
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete | ||
{ | ||
public class TestContextWriteIsObsoleteAnalyzerTests | ||
{ | ||
private static readonly DiagnosticAnalyzer analyzer = new TestContextWriteIsObsoleteAnalyzer(); | ||
private static readonly ExpectedDiagnostic expectedDiagnostic = | ||
ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestContextWriteIsObsolete); | ||
|
||
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))] | ||
public void AnyDirectWriteMethod(string writeMethodAndParameters) | ||
{ | ||
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" | ||
public void Test() | ||
{{ | ||
TestContext.{writeMethodAndParameters}; | ||
}}"); | ||
|
||
RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); | ||
} | ||
|
||
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))] | ||
public void AnyIndirectWriteMethod(string writeMethodAndParameters) | ||
{ | ||
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" | ||
public void Test() | ||
{{ | ||
TestContext.Out.{writeMethodAndParameters}; | ||
}}"); | ||
|
||
RoslynAssert.Valid(analyzer, testCode); | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
...unit.analyzers.tests/TestContextWriteIsObsolete/TestContextWriteIsObsoleteCodeFixTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using Gu.Roslyn.Asserts; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using NUnit.Analyzers.Constants; | ||
using NUnit.Analyzers.TestContextWriteIsObsolete; | ||
using NUnit.Framework; | ||
|
||
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete | ||
{ | ||
public class TestContextWriteIsObsoleteCodeFixTests | ||
{ | ||
private static readonly DiagnosticAnalyzer analyzer = new TestContextWriteIsObsoleteAnalyzer(); | ||
private static readonly CodeFixProvider fix = new TestContextWriteIsObsoleteCodeFix(); | ||
private static readonly ExpectedDiagnostic expectedDiagnostic = | ||
ExpectedDiagnostic.Create(AnalyzerIdentifiers.TestContextWriteIsObsolete); | ||
|
||
[TestCaseSource(typeof(TestContextWriteIsObsoleteTestCases), nameof(TestContextWriteIsObsoleteTestCases.WriteInvocations))] | ||
public void AnyWriteMethod(string writeMethodAndParameters) | ||
{ | ||
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" | ||
public void Test() | ||
{{ | ||
TestContext.{writeMethodAndParameters}; | ||
}}"); | ||
|
||
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" | ||
public void Test() | ||
{{ | ||
TestContext.Out.{writeMethodAndParameters}; | ||
}}"); | ||
|
||
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, | ||
fixTitle: TestContextWriteIsObsoleteCodeFix.InsertOutDescription); | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
src/nunit.analyzers.tests/TestContextWriteIsObsolete/TestContextWriteIsObsoleteTestCases.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
namespace NUnit.Analyzers.Tests.TestContextWriteIsObsolete | ||
{ | ||
internal static class TestContextWriteIsObsoleteTestCases | ||
{ | ||
public static readonly string[] WriteInvocations = | ||
{ | ||
"Write(true)", | ||
"Write('!')", | ||
"Write(new char[] { '!', '!' })", | ||
"Write(default(char[]))", | ||
"Write(1D)", | ||
"Write(1)", | ||
"Write(1L)", | ||
"Write(1M)", | ||
"Write(default(object))", | ||
"Write(1F)", | ||
"Write(\"NUnit\")", | ||
"Write(default(string))", | ||
"Write(1U)", | ||
"Write(1UL)", | ||
"Write(\"{0}\", 1)", | ||
"Write(\"{0} + {1}\", 1, 2)", | ||
"Write(\"{0} + {1} = {2}\", 1, 2, 3)", | ||
"Write(\"{0} + {1} = {2} + {3}\", 1, 2, 2, 1)", | ||
"WriteLine()", | ||
"WriteLine(true)", | ||
"WriteLine('!')", | ||
"WriteLine(new char[] { '!', '!' })", | ||
"WriteLine(default(char[]))", | ||
"WriteLine(1D)", | ||
"WriteLine(1)", | ||
"WriteLine(1L)", | ||
"WriteLine(1M)", | ||
"WriteLine(default(object))", | ||
"WriteLine(1F)", | ||
"WriteLine(\"NUnit\")", | ||
"Write(default(string))", | ||
"WriteLine(1U)", | ||
"WriteLine(1UL)", | ||
"WriteLine(\"{0}\", 1)", | ||
"WriteLine(\"{0} + {1}\", 1, 2)", | ||
"WriteLine(\"{0} + {1} = {2}\", 1, 2, 3)", | ||
"WriteLine(\"{0} + {1} = {2} + {3}\", 1, 2, 2, 1)", | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
src/nunit.analyzers/TestContextWriteIsObsolete/TestContextWriteIsObsoleteAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
using System.Collections.Immutable; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Operations; | ||
using NUnit.Analyzers.Constants; | ||
|
||
namespace NUnit.Analyzers.TestContextWriteIsObsolete | ||
{ | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class TestContextWriteIsObsoleteAnalyzer : DiagnosticAnalyzer | ||
{ | ||
private static readonly DiagnosticDescriptor descriptor = DiagnosticDescriptorCreator.Create( | ||
id: AnalyzerIdentifiers.TestContextWriteIsObsolete, | ||
title: TestContextWriteIsObsoleteAnalyzerConstants.Title, | ||
messageFormat: TestContextWriteIsObsoleteAnalyzerConstants.Message, | ||
category: Categories.Structure, | ||
defaultSeverity: DiagnosticSeverity.Error, | ||
description: TestContextWriteIsObsoleteAnalyzerConstants.Description); | ||
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(descriptor); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterCompilationStartAction(AnalyzeCompilationStart); | ||
} | ||
|
||
private static void AnalyzeCompilationStart(CompilationStartAnalysisContext context) | ||
{ | ||
INamedTypeSymbol? testContextType = context.Compilation.GetTypeByMetadataName(NUnitFrameworkConstants.FullNameOfTypeTestContext); | ||
if (testContextType is null) | ||
{ | ||
return; | ||
} | ||
|
||
context.RegisterOperationAction(context => AnalyzeInvocation(testContextType, context), OperationKind.Invocation); | ||
} | ||
|
||
private static void AnalyzeInvocation(INamedTypeSymbol testContextType, OperationAnalysisContext context) | ||
{ | ||
if (context.Operation is not IInvocationOperation invocationOperation) | ||
return; | ||
|
||
// TestContext.Write methods are static methods | ||
if (invocationOperation.Instance is not null) | ||
return; | ||
|
||
IMethodSymbol targetMethod = invocationOperation.TargetMethod; | ||
|
||
if (!targetMethod.ReturnsVoid) | ||
return; | ||
|
||
context.CancellationToken.ThrowIfCancellationRequested(); | ||
|
||
if (!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, testContextType)) | ||
return; | ||
|
||
if (targetMethod.Name is NUnitFrameworkConstants.NameOfWrite or | ||
NUnitFrameworkConstants.NameOfWriteLine) | ||
{ | ||
context.ReportDiagnostic(Diagnostic.Create( | ||
descriptor, | ||
invocationOperation.Syntax.GetLocation())); | ||
} | ||
} | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
...nunit.analyzers/TestContextWriteIsObsolete/TestContextWriteIsObsoleteAnalyzerConstants.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace NUnit.Analyzers.TestContextWriteIsObsolete | ||
{ | ||
internal static class TestContextWriteIsObsoleteAnalyzerConstants | ||
{ | ||
public const string Title = "The Write methods on TestContext are Obsolete"; | ||
public const string Message = "The Write methods are wrappers on TestContext.Out"; | ||
public const string Description = "Direct Write calls should be replaced with Out.Write."; | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
src/nunit.analyzers/TestContextWriteIsObsolete/TestContextWriteIsObsoleteCodeFix.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using NUnit.Analyzers.Constants; | ||
|
||
namespace NUnit.Analyzers.TestContextWriteIsObsolete | ||
{ | ||
[ExportCodeFixProvider(LanguageNames.CSharp)] | ||
[Shared] | ||
public class TestContextWriteIsObsoleteCodeFix : CodeFixProvider | ||
{ | ||
internal const string InsertOutDescription = "Replace TestContext.Write with TestContext.Out.Write"; | ||
|
||
public override ImmutableArray<string> FixableDiagnosticIds { get; } | ||
= ImmutableArray.Create(AnalyzerIdentifiers.TestContextWriteIsObsolete); | ||
|
||
public sealed override FixAllProvider GetFixAllProvider() | ||
{ | ||
return WellKnownFixAllProviders.BatchFixer; | ||
} | ||
|
||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
|
||
if (root is null) | ||
{ | ||
return; | ||
} | ||
|
||
context.CancellationToken.ThrowIfCancellationRequested(); | ||
|
||
var node = root.FindNode(context.Span); | ||
var invocationNode = node as InvocationExpressionSyntax; | ||
if (invocationNode is null) | ||
return; | ||
|
||
var memberAccessExpression = invocationNode.Expression as MemberAccessExpressionSyntax; | ||
if (memberAccessExpression is null) | ||
return; | ||
|
||
var updatedMemberAccessExpression = | ||
SyntaxFactory.MemberAccessExpression( | ||
memberAccessExpression.Kind(), | ||
SyntaxFactory.MemberAccessExpression( | ||
SyntaxKind.SimpleMemberAccessExpression, | ||
memberAccessExpression.Expression, | ||
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfOut)), | ||
memberAccessExpression.Name); | ||
|
||
var newRoot = root.ReplaceNode(memberAccessExpression, updatedMemberAccessExpression); | ||
|
||
var codeAction = CodeAction.Create( | ||
InsertOutDescription, | ||
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)), | ||
InsertOutDescription); | ||
|
||
context.RegisterCodeFix(codeAction, context.Diagnostics); | ||
} | ||
} | ||
} |