Skip to content

Commit

Permalink
Add CodeFix to add using (Assert.EnterMultipleScope())
Browse files Browse the repository at this point in the history
Allow the user to select the Assert.Multiple codefix
manfred-brands committed Jan 8, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 804395a commit cf7eacb
Showing 4 changed files with 178 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ public void TestMethod()
Assert.That(false, Is.False);
Console.WriteLine(""Next Statement"");
}");

var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{
@@ -44,12 +45,36 @@ public void TestMethod()
});
Console.WriteLine(""Next Statement"");
}");
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);

#if NUNIT4
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{
using (Assert.EnterMultipleScope())
{
Assert.That(true, Is.True);
Assert.That(false, Is.False);
}
Console.WriteLine(""Next Statement"");
}");

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
#endif
}

[Test]
public void VerifyPartlyIndependent()
{
const string ConfigurationClass = @"
private sealed class Configuration
{
public int Value1 { get; set; }
public double Value2 { get; set; }
public string Value11 { get; set; } = string.Empty;
}";

var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void Test()
{
@@ -59,14 +84,8 @@ public void Test()
Assert.That(configuration.Value2, Is.EqualTo(0.0));
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
configuration = null;
}
}" + ConfigurationClass);

private sealed class Configuration
{
public int Value1 { get; set; }
public double Value2 { get; set; }
public string Value11 { get; set; } = string.Empty;
}");
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void Test()
{
@@ -79,40 +98,54 @@ public void Test()
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
});
configuration = null;
}" + ConfigurationClass);

RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);

#if NUNIT4
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void Test()
{
var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
using (Assert.EnterMultipleScope())
{
Assert.That(configuration.Value1, Is.EqualTo(0));
Assert.That(configuration.Value2, Is.EqualTo(0.0));
Assert.That(configuration.Value11, Is.EqualTo(string.Empty));
}
configuration = null;
}" + ConfigurationClass);

RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
#endif
}

[Test]
public void AddsAsyncWhenAwaitIsUsed()
{
const string ConfigurationClass = @"
private sealed class Configuration
{
public int Value1 { get; set; }
public double Value2 { get; set; }
public string Value11 { get; set; } = string.Empty;
}");
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode);
}
public Task<string> AsStringAsync() => Task.FromResult(Value11);
}";

[Test]
public void AddsAsyncWhenAwaitIsUsed()
{
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void Test()
public async Task Test()
{
var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
↓Assert.That(configuration.Value1, Is.EqualTo(0));
Assert.That(configuration.Value2, Is.EqualTo(0.0));
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
configuration = null;
}
}" + ConfigurationClass);

private sealed class Configuration
{
public int Value1 { get; set; }
public double Value2 { get; set; }
public string Value11 { get; set; } = string.Empty;
public Task<string> AsStringAsync() => Task.FromResult(Value11);
}");
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void Test()
public async Task Test()
{
var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
@@ -123,16 +156,30 @@ public void Test()
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
});
configuration = null;
}
}" + ConfigurationClass);

private sealed class Configuration
// The test method itself no longer awaits, so CS1998 is generated.
// Fixing this is outside the scope of this analyzer and there could be other non-touched statements that are waited.
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple,
Settings.Default.WithAllowedCompilerDiagnostics(AllowedCompilerDiagnostics.WarningsAndErrors));

#if NUNIT4
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public async Task Test()
{
public int Value1 { get; set; }
public double Value2 { get; set; }
public string Value11 { get; set; } = string.Empty;
public Task<string> AsStringAsync() => Task.FromResult(Value11);
}");
RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode);
var configuration = new Configuration();
Assert.That(configuration, Is.Not.Null);
using (Assert.EnterMultipleScope())
{
Assert.That(configuration.Value1, Is.EqualTo(0));
Assert.That(configuration.Value2, Is.EqualTo(0.0));
Assert.That(await configuration.AsStringAsync(), Is.EqualTo(string.Empty));
}
configuration = null;
}" + ConfigurationClass);

RoslynAssert.FixAll(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
#endif
}

[Test]
@@ -152,6 +199,7 @@ public void TestMethod()
Assert.That(False, Is.False);{newline}
{preComment}Console.WriteLine(""Next Statement"");{postComment}
}}");

var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
public void TestMethod()
{{
@@ -166,30 +214,67 @@ public void TestMethod()
}});{newline}
{preComment}Console.WriteLine(""Next Statement"");{postComment}
}}");
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);

#if NUNIT4
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
public void TestMethod()
{{
const bool True = true;
const bool False = false;
using (Assert.EnterMultipleScope())
{{
// Verify that our bool constants are correct
Assert.That(True, Is.True);
Assert.That(False, Is.False);
}}{newline}
{preComment}Console.WriteLine(""Next Statement"");{postComment}
}}");

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
#endif
}

[Test]
public void VerifyKeepsTrivia()
{
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
var code = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{{
// Verify that boolean work as expected
{
// Verify that boolean work as expected
↓Assert.That(true, Is.True);
Assert.That(false, Is.False);
}}");
var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$"
}");

var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{{
{
Assert.Multiple(() =>
{{
{
// Verify that boolean work as expected
Assert.That(true, Is.True);
Assert.That(false, Is.False);
}});
}}");
RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode);
});
}");

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertMultiple);

#if NUNIT4
fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{
using (Assert.EnterMultipleScope())
{
// Verify that boolean work as expected
Assert.That(true, Is.True);
Assert.That(false, Is.False);
}
}");

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, UseAssertMultipleCodeFix.WrapWithAssertEnterMultipleScope);
#endif
}
}
}
2 changes: 2 additions & 0 deletions src/nunit.analyzers/Constants/AnalyzerPropertyKeys.cs
Original file line number Diff line number Diff line change
@@ -5,5 +5,7 @@ internal static class AnalyzerPropertyKeys
internal const string ModelName = nameof(AnalyzerPropertyKeys.ModelName);
internal const string ArgsIsArray = nameof(AnalyzerPropertyKeys.ArgsIsArray);
internal const string MinimumNumberOfArguments = nameof(AnalyzerPropertyKeys.MinimumNumberOfArguments);

internal const string SupportsEnterMultipleScope = nameof(AnalyzerPropertyKeys.SupportsEnterMultipleScope);
}
}
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ namespace NUnit.Analyzers.UseAssertMultiple
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UseAssertMultipleAnalyzer : BaseAssertionAnalyzer
{
private static readonly Version firstNUnitVersionWithEnterMultipleScope = new Version(4, 2);

private static readonly DiagnosticDescriptor descriptor = DiagnosticDescriptorCreator.Create(
id: AnalyzerIdentifiers.UseAssertMultiple,
title: UseAssertMultipleConstants.Title,
@@ -67,7 +69,7 @@ internal static void Add(HashSet<string> previousArguments, string argument)
}
}

protected override void AnalyzeAssertInvocation(OperationAnalysisContext context, IInvocationOperation assertOperation)
protected override void AnalyzeAssertInvocation(Version nunitVersion, OperationAnalysisContext context, IInvocationOperation assertOperation)
{
if (assertOperation.TargetMethod.Name != NUnitFrameworkConstants.NameOfAssertThat ||
AssertHelper.IsInsideAssertMultiple(assertOperation.Syntax))
@@ -134,7 +136,11 @@ protected override void AnalyzeAssertInvocation(OperationAnalysisContext context

if (lastAssert > firstAssert)
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, assertOperation.Syntax.GetLocation()));
var properties = ImmutableDictionary.CreateBuilder<string, string?>();
properties.Add(AnalyzerPropertyKeys.SupportsEnterMultipleScope,
nunitVersion >= firstNUnitVersionWithEnterMultipleScope ?
NUnitFrameworkConstants.NameOfEnterMultipleScope : null);
context.ReportDiagnostic(Diagnostic.Create(descriptor, assertOperation.Syntax.GetLocation(), properties.ToImmutable()));
}
}
}
56 changes: 40 additions & 16 deletions src/nunit.analyzers/UseAssertMultiple/UseAssertMultipleCodeFix.cs
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ namespace NUnit.Analyzers.UseAssertMultiple
[ExportCodeFixProvider(LanguageNames.CSharp)]
public class UseAssertMultipleCodeFix : CodeFixProvider
{
internal const string WrapWithAssertEnterMultipleScope = "Wrap with 'using (Assert.EnterMultipleScope())' statement";
internal const string WrapWithAssertMultiple = "Wrap with Assert.Multiple call";

public override ImmutableArray<string> FixableDiagnosticIds
@@ -106,6 +107,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
}
}

var diagnostic = context.Diagnostics.First();
bool supportedEnterMultipleScope = diagnostic.Properties[AnalyzerPropertyKeys.SupportsEnterMultipleScope] is not null;
if (supportedEnterMultipleScope)
{
UsingStatementSyntax usingAssertEnterMultipleScope =
SyntaxFactory.UsingStatement(null,
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfAssert),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfEnterMultipleScope))),
SyntaxFactory.Block(statementsInsideAssertMultiple))
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
.WithAdditionalAnnotations(Formatter.Annotation);

RegisterCodeFix(WrapWithAssertEnterMultipleScope, usingAssertEnterMultipleScope);
}

ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpression =
SyntaxFactory.ParenthesizedLambdaExpression(
SyntaxFactory.Block(statementsInsideAssertMultiple));
@@ -130,27 +149,32 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
.WithAdditionalAnnotations(Formatter.Annotation);

if (endOfLineTrivia is not null)
RegisterCodeFix(WrapWithAssertMultiple, assertMultiple);

void RegisterCodeFix(string name, SyntaxNode assertMultiple)
{
// Add the remembered blank line to go before the Assert.Multiple statement.
assertMultiple = assertMultiple.WithLeadingTrivia(endOfLineTrivia.Value);
}
if (endOfLineTrivia is not null)
{
// Add the remembered blank line to go before the Assert.Multiple statement.
assertMultiple = assertMultiple.WithLeadingTrivia(endOfLineTrivia.Value);
}

// Comments at the end of a block are not associated with the last statement but with the closing brace
// Keep the exising block's open and close braces with associated trivia in our updated block.
var updatedBlock = SyntaxFactory.Block(
block.OpenBraceToken,
SyntaxFactory.List(statementsBeforeAssertMultiple.Append(assertMultiple).Concat(statementsAfterAssertMultiple)),
block.CloseBraceToken);
// Comments at the end of a block are not associated with the last statement but with the closing brace
// Keep the exising block's open and close braces with associated trivia in our updated block.
var updatedBlock = SyntaxFactory.Block(
block.OpenBraceToken,
SyntaxFactory.List(statementsBeforeAssertMultiple.Append(assertMultiple).Concat(statementsAfterAssertMultiple)),
block.CloseBraceToken);

SyntaxNode newRoot = root.ReplaceNode(block, updatedBlock);
SyntaxNode newRoot = root.ReplaceNode(block, updatedBlock);

var codeAction = CodeAction.Create(
WrapWithAssertMultiple,
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)),
WrapWithAssertMultiple);
var codeAction = CodeAction.Create(
name,
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)),
name);

context.RegisterCodeFix(codeAction, context.Diagnostics);
context.RegisterCodeFix(codeAction, context.Diagnostics);
}
}
}
}

0 comments on commit cf7eacb

Please sign in to comment.