Skip to content

Commit

Permalink
Optimize Are(Not)Equal when actually wanting to test for Empty.
Browse files Browse the repository at this point in the history
  • Loading branch information
manfred-brands committed Dec 22, 2024
1 parent b2f4e68 commit 3095eae
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Gu.Roslyn.Asserts;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Analyzers.ClassicModelAssertUsage;
Expand Down Expand Up @@ -350,5 +354,60 @@ public void CodeFixMaintainsReasonableTriviaWithAllArgumentsOnSameLine([Values]

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
}

[TestCase("string.Empty")]
[TestCase("String.Empty")]
[TestCase("Guid.Empty")]
[TestCase("\"\"")]
[TestCase("Array.Empty<int>()")]
[TestCase("Enumerable.Empty<int>()", "using System.Linq;")]
public void CodeFixUsesIsEmpty(string expected, string? additionalUsings = null)
{
var code = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
↓ClassicAssert.AreEqual({expected}, value);",
additionalUsings);

var fixedCode = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
Assert.That(value, Is.Empty);",
additionalUsings);

IEnumerable<MetadataReference> existingReferences = Settings.Default.MetadataReferences ?? Enumerable.Empty<MetadataReference>();

Settings settings = Settings.Default
.WithMetadataReferences(existingReferences.Concat(MetadataReferences.Transitive(typeof(ImmutableArray<>))));

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode,
fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription,
settings: settings);
}

[TestCase("ImmutableArray<int>.Empty")]
[TestCase("ImmutableList<int>.Empty")]
[TestCase("ImmutableHashSet<int>.Empty")]
public void CodeFixUsesIsEmpty(string expected)
{
const string UsingSystemCollectionsImmutable = "using System.Collections.Immutable;";

var code = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
↓ClassicAssert.AreEqual({expected}, value);",
UsingSystemCollectionsImmutable);

var fixedCode = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
Assert.That(value, Is.Empty);",
UsingSystemCollectionsImmutable);

IEnumerable<MetadataReference> existingReferences = Settings.Default.MetadataReferences ?? Enumerable.Empty<MetadataReference>();

Settings settings = Settings.Default
.WithMetadataReferences(existingReferences.Concat(MetadataReferences.Transitive(typeof(ImmutableArray<>))));

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode,
fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription,
settings: settings);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Gu.Roslyn.Asserts;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Analyzers.ClassicModelAssertUsage;
Expand Down Expand Up @@ -158,5 +162,60 @@ public void CodeFixMaintainsReasonableTriviaWithAllArgumentsOnSameLine([Values]

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode, fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription);
}

[TestCase("string.Empty")]
[TestCase("String.Empty")]
[TestCase("Guid.Empty")]
[TestCase("\"\"")]
[TestCase("Array.Empty<int>()")]
[TestCase("Enumerable.Empty<int>()", "using System.Linq;")]
public void CodeFixUsesIsEmpty(string expected, string? additionalUsings = null)
{
var code = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
↓ClassicAssert.AreNotEqual({expected}, value);",
additionalUsings);

var fixedCode = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
Assert.That(value, Is.Not.Empty);",
additionalUsings);

IEnumerable<MetadataReference> existingReferences = Settings.Default.MetadataReferences ?? Enumerable.Empty<MetadataReference>();

Settings settings = Settings.Default
.WithMetadataReferences(existingReferences.Concat(MetadataReferences.Transitive(typeof(ImmutableArray<>))));

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode,
fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription,
settings: settings);
}

[TestCase("ImmutableArray<int>.Empty")]
[TestCase("ImmutableList<int>.Empty")]
[TestCase("ImmutableHashSet<int>.Empty")]
public void CodeFixUsesIsEmpty(string expected)
{
const string UsingSystemCollectionsImmutable = "using System.Collections.Immutable;";

var code = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
↓ClassicAssert.AreNotEqual({expected}, value);",
UsingSystemCollectionsImmutable);

var fixedCode = TestUtility.WrapInTestMethod($@"
string value = ""Value"";
Assert.That(value, Is.Not.Empty);",
UsingSystemCollectionsImmutable);

IEnumerable<MetadataReference> existingReferences = Settings.Default.MetadataReferences ?? Enumerable.Empty<MetadataReference>();

Settings settings = Settings.Default
.WithMetadataReferences(existingReferences.Concat(MetadataReferences.Transitive(typeof(ImmutableArray<>))));

RoslynAssert.CodeFix(analyzer, fix, expectedDiagnostic, code, fixedCode,
fixTitle: ClassicModelAssertUsageCodeFix.TransformToConstraintModelDescription,
settings: settings);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
Expand All @@ -6,6 +7,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Analyzers.Constants;
using NUnit.Analyzers.Helpers;

namespace NUnit.Analyzers.ClassicModelAssertUsage
{
Expand All @@ -21,32 +23,45 @@ protected override (ArgumentSyntax ActualArgument, ArgumentSyntax? ConstraintArg
IReadOnlyDictionary<string, ArgumentSyntax> argumentNamesToArguments)
{
var expectedArgument = argumentNamesToArguments[NUnitFrameworkConstants.NameOfExpectedParameter].WithNameColon(null);
var equalToInvocationNode = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(

ExpressionSyntax constraint;

if (CodeFixHelper.IsEmpty(expectedArgument.Expression))
{
constraint = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIs),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEqualTo)))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(expectedArgument.WithoutTrivia())));

// The tolerance argument has to be added to the "Is.EqualTo(expected)" as ".Within(tolerance)"
if (argumentNamesToArguments.TryGetValue(NUnitFrameworkConstants.NameOfDeltaParameter, out var toleranceArgument))
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEmpty));
}
else
{
// The tolerance argument should be renamed from 'delta' to 'amount' but with the model constraint the
// argument is moved to Within which makes it way more explicit so we can just drop the name colon.
var toleranceArgumentNoColon = toleranceArgument.WithNameColon(null);

equalToInvocationNode = SyntaxFactory.InvocationExpression(
constraint = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
equalToInvocationNode,
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfEqualConstraintWithin)))
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIs),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEqualTo)))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(toleranceArgumentNoColon.WithoutTrivia())));
SyntaxFactory.SingletonSeparatedList(expectedArgument.WithoutTrivia())));

// The tolerance argument has to be added to the "Is.EqualTo(expected)" as ".Within(tolerance)"
if (argumentNamesToArguments.TryGetValue(NUnitFrameworkConstants.NameOfDeltaParameter, out var toleranceArgument))
{
// The tolerance argument should be renamed from 'delta' to 'amount' but with the model constraint the
// argument is moved to Within which makes it way more explicit so we can just drop the name colon.
var toleranceArgumentNoColon = toleranceArgument.WithNameColon(null);

constraint = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
constraint,
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfEqualConstraintWithin)))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(toleranceArgumentNoColon.WithoutTrivia())));
}
}

var actualArgument = argumentNamesToArguments[NUnitFrameworkConstants.NameOfActualParameter].WithNameColon(null);
return (actualArgument, SyntaxFactory.Argument(equalToInvocationNode));
return (actualArgument, SyntaxFactory.Argument(constraint));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NUnit.Analyzers.Constants;
using NUnit.Analyzers.Helpers;

namespace NUnit.Analyzers.ClassicModelAssertUsage
{
Expand All @@ -21,8 +22,21 @@ protected override (ArgumentSyntax ActualArgument, ArgumentSyntax? ConstraintArg
IReadOnlyDictionary<string, ArgumentSyntax> argumentNamesToArguments)
{
var expectedArgument = argumentNamesToArguments[NUnitFrameworkConstants.NameOfExpectedParameter].WithNameColon(null);
var constraintArgument = SyntaxFactory.Argument(
SyntaxFactory.InvocationExpression(
ExpressionSyntax constraint;

if (CodeFixHelper.IsEmpty(expectedArgument.Expression))
{
constraint = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIs),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsNot)),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEmpty));
}
else
{
constraint = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.MemberAccessExpression(
Expand All @@ -31,10 +45,11 @@ protected override (ArgumentSyntax ActualArgument, ArgumentSyntax? ConstraintArg
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsNot)),
SyntaxFactory.IdentifierName(NUnitFrameworkConstants.NameOfIsEqualTo)))
.WithArgumentList(SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(expectedArgument))));
SyntaxFactory.SingletonSeparatedList(expectedArgument)));
}

var actualArgument = argumentNamesToArguments[NUnitFrameworkConstants.NameOfActualParameter].WithNameColon(null);
return (actualArgument, constraintArgument);
return (actualArgument, SyntaxFactory.Argument(constraint));
}
}
}
30 changes: 30 additions & 0 deletions src/nunit.analyzers/Helpers/CodeFixHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -129,6 +130,35 @@ public static void UpdateStringFormatToFormattableString(List<ArgumentSyntax> ar
arguments.RemoveRange(nextArgument, arguments.Count - nextArgument);
}

/// <summary>
/// Checks if the given expression is something that is covered by the EmptyConstraint.
/// </summary>
internal static bool IsEmpty(ExpressionSyntax expression)
{
if (expression is LiteralExpressionSyntax literalExpression && literalExpression.Token.ValueText.Length == 0)
{
return true;
}

if (expression is MemberAccessExpressionSyntax memberProperty &&
memberProperty.Name.Identifier.Text == nameof(string.Empty))
{
// Intends to covers string.Empty, ImmutableXXX<T>.Empty and other similar cases.
return true;
}

if (expression is InvocationExpressionSyntax invocationExpression &&
invocationExpression.ArgumentList.Arguments.Count == 0 &&
invocationExpression.Expression is MemberAccessExpressionSyntax memberMethod &&
memberMethod.Name is GenericNameSyntax genericName && genericName.Identifier.Text == nameof(Array.Empty))
{
// Intends to covers Array.Empty<T>(), Enumerable.Empty<T> and other similar cases.
return true;
}

return false;
}

internal static IEnumerable<InterpolatedStringContentSyntax> UpdateStringFormatToFormattableString(string formatSpecification, ExpressionSyntax[] formatArguments)
{
int startIndex = 0;
Expand Down

0 comments on commit 3095eae

Please sign in to comment.