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
  • Loading branch information
manfred-brands committed Jan 8, 2025
1 parent 804395a commit cf7eacb
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public void TestMethod()
Assert.That(false, Is.False);
Console.WriteLine(""Next Statement"");
}");

var fixedCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@"
public void TestMethod()
{
Expand All @@ -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()
{
Expand All @@ -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()
{
Expand All @@ -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);
Expand All @@ -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]
Expand All @@ -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()
{{
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()));
}
}
}
Expand Down
56 changes: 40 additions & 16 deletions src/nunit.analyzers/UseAssertMultiple/UseAssertMultipleCodeFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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.