Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Verifier = TUnit.Assertions.Analyzers.CodeFixers.Tests.Verifiers.CSharpCodeFixVerifier<
TUnit.Assertions.Analyzers.CollectionIsEqualToAnalyzer,
TUnit.Assertions.Analyzers.CodeFixers.CollectionIsEqualToCodeFixProvider>;

namespace TUnit.Assertions.Analyzers.CodeFixers.Tests;

public class CollectionIsEqualToCodeFixProviderTests
{
[Test]
public async Task Rewrites_IsEqualTo_To_IsEquivalentTo()
{
await Verifier
.VerifyCodeFixAsync(
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var a = new List<int> { 1, 2 };
var b = new List<int> { 1, 2 };
await Assert.That(a).{|#0:IsEqualTo(b)|};
}
}
""",
Verifier.Diagnostic(Rules.CollectionIsEqualToUsesReferenceEquality)
.WithLocation(0),
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var a = new List<int> { 1, 2 };
var b = new List<int> { 1, 2 };
await Assert.That(a).IsEquivalentTo(b);
}
}
"""
);
}

[Test]
public async Task Fix_Preserves_Chained_Calls()
{
await Verifier.VerifyCodeFixAsync(
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var a = new List<int> { 1, 2 };
var b = new List<int> { 1, 2 };
await Assert.That(a).{|#0:IsEqualTo(b)|}.And.IsNotNull();
}
}
""",
Verifier.Diagnostic(Rules.CollectionIsEqualToUsesReferenceEquality).WithLocation(0),
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var a = new List<int> { 1, 2 };
var b = new List<int> { 1, 2 };
await Assert.That(a).IsEquivalentTo(b).And.IsNotNull();
}
}
""");
}

[Test]
public async Task Fix_Works_On_Arrays()
{
await Verifier.VerifyCodeFixAsync(
"""
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
int[] a = { 1 };
int[] b = { 1 };
await Assert.That(a).{|#0:IsEqualTo(b)|};
}
}
""",
Verifier.Diagnostic(Rules.CollectionIsEqualToUsesReferenceEquality).WithLocation(0),
"""
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
int[] a = { 1 };
int[] b = { 1 };
await Assert.That(a).IsEquivalentTo(b);
}
}
""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace TUnit.Assertions.Analyzers.CodeFixers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionIsEqualToCodeFixProvider)), Shared]
public class CollectionIsEqualToCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(Rules.CollectionIsEqualToUsesReferenceEquality.Id);

public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

foreach (var diagnostic in context.Diagnostics)
{
// Analyzer reports a span covering `IsEqualTo(...)`; FindNode returns the enclosing invocation.
if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true)
is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name: IdentifierNameSyntax identifier } })
{
continue;
}

context.RegisterCodeFix(
CodeAction.Create(
title: Resources.TUnitAssertions0016CodeFixTitle,
createChangedDocument: c => ReplaceAsync(context.Document, identifier, c),
equivalenceKey: nameof(Resources.TUnitAssertions0016CodeFixTitle)),
diagnostic);
}
}

private static async Task<Document> ReplaceAsync(Document document, IdentifierNameSyntax identifier, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is null)
{
return document;
}

var replacement = SyntaxFactory
.IdentifierName("IsEquivalentTo")
.WithTriviaFrom(identifier);

return document.WithSyntaxRoot(root.ReplaceNode(identifier, replacement));
}
}
193 changes: 193 additions & 0 deletions TUnit.Assertions.Analyzers.Tests/CollectionIsEqualToAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using Verifier = TUnit.Assertions.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.Assertions.Analyzers.CollectionIsEqualToAnalyzer>;

namespace TUnit.Assertions.Analyzers.Tests;

public class CollectionIsEqualToAnalyzerTests
{
[Test]
public async Task List_IsEqualTo_Raises_Info()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var a = new List<int> { 1, 2, 3 };
var b = new List<int> { 1, 2, 3 };
await Assert.That(a).{|#0:IsEqualTo(b)|};
}
}
""",
Verifier.Diagnostic(Rules.CollectionIsEqualToUsesReferenceEquality)
.WithLocation(0)
);
}

[Test]
public async Task String_Not_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
await Assert.That("abc").IsEqualTo("abc");
}
}
"""
);
}

[Test]
public async Task Int_Not_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test() => await Assert.That(1).IsEqualTo(1);
}
"""
);
}

[Test]
public async Task Array_IsEqualTo_Raises_Info()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
int[] a = { 1, 2 };
int[] b = { 1, 2 };
await Assert.That(a).{|#0:IsEqualTo(b)|};
}
}
""",
Verifier.Diagnostic(Rules.CollectionIsEqualToUsesReferenceEquality)
.WithLocation(0)
);
}

[Test]
public async Task Count_IsEqualTo_Not_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyClass
{
[Test]
public async Task Test()
{
var list = new List<int> { 1, 2 };
await Assert.That(list).Count().IsEqualTo(2);
}
}
"""
);
}

[Test]
public async Task CustomEnumerable_With_EqualsOverride_Not_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public class MyBag : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() => new List<int>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override bool Equals(object? obj) => obj is MyBag;
public override int GetHashCode() => 0;
}

public class MyClass
{
[Test]
public async Task Test()
{
await Assert.That(new MyBag()).IsEqualTo(new MyBag());
}
}
"""
);
}

[Test]
public async Task Record_Collection_Not_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

public record MyRecordBag(int X) : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() => new List<int> { X }.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class MyClass
{
[Test]
public async Task Test()
{
await Assert.That(new MyRecordBag(1)).IsEqualTo(new MyRecordBag(1));
}
}
"""
);
}
}
1 change: 1 addition & 0 deletions TUnit.Assertions.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TUnitAssertions0014 | Usage | Warning | Prefer IsNull() over IsEqualTo(null)
TUnitAssertions0015 | Usage | Warning | Prefer IsTrue()/IsFalse() over IsEqualTo(true/false)
TUnitAssertions0016 | Usage | Info | Collection IsEqualTo compares by reference - use IsEquivalentTo
Loading
Loading