Skip to content

Commit

Permalink
introduce single CollectionAnalyzer for collection assertions (#251)
Browse files Browse the repository at this point in the history
* introduce single CollectionAnalyzer for collection assertions

* Restore help links for collection assertions

* cleanup

* consolidate CollectionShouldNotContainProperty into collection analyzer

* Update CollectionTests.cs and remove CollectionShouldHaveElementAt0Null.cs

* Refactor collection intersection code

* revert some changes

* update TODOs

* cleanup collections
  • Loading branch information
Meir017 authored Dec 12, 2023
1 parent 745171a commit dd3a22e
Show file tree
Hide file tree
Showing 33 changed files with 413 additions and 1,160 deletions.
112 changes: 56 additions & 56 deletions src/FluentAssertions.Analyzers.Tests/Tips/CollectionTests.cs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/FluentAssertions.Analyzers.Tests/Tips/SanityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@ public class TestType3

DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(new[] { source }, new DiagnosticResult()
{
Id = CollectionShouldNotBeEmptyAnalyzer.DiagnosticId,
Message = CollectionShouldNotBeEmptyAnalyzer.Message,
Id = CollectionAnalyzer.DiagnosticId,
Message = CollectionAnalyzer.Message,
Severity = DiagnosticSeverity.Info,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 9) }
});
Expand Down
27 changes: 1 addition & 26 deletions src/FluentAssertions.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public static class Constants
{
public static class DiagnosticProperties
{
public const string RuleId = nameof(RuleId);
public const string Title = nameof(Title);
public const string VisitorName = nameof(VisitorName);
public const string HelpLink = nameof(HelpLink);
Expand All @@ -15,34 +16,8 @@ public static class Tips
public const string Category = "FluentAssertionTips";
public static class Collections
{
public const string CollectionsShouldBeEmpty = $"{DiagnosticProperties.IdPrefix}0000";
public const string CollectionsShouldNotBeEmpty = $"{DiagnosticProperties.IdPrefix}0001";
public const string CollectionShouldContainProperty = $"{DiagnosticProperties.IdPrefix}0002";
public const string CollectionShouldNotContainProperty = $"{DiagnosticProperties.IdPrefix}0003";
public const string CollectionShouldContainItem = $"{DiagnosticProperties.IdPrefix}0004";
public const string CollectionShouldNotContainItem = $"{DiagnosticProperties.IdPrefix}0005";
public const string CollectionShouldHaveCount = $"{DiagnosticProperties.IdPrefix}0006";
public const string CollectionShouldHaveCountGreaterThan = $"{DiagnosticProperties.IdPrefix}0007";
public const string CollectionShouldHaveCountGreaterOrEqualTo = $"{DiagnosticProperties.IdPrefix}0008";
public const string CollectionShouldHaveCountLessThan = $"{DiagnosticProperties.IdPrefix}0009";
public const string CollectionShouldHaveCountLessOrEqualTo = $"{DiagnosticProperties.IdPrefix}0010";
public const string CollectionShouldNotHaveCount = $"{DiagnosticProperties.IdPrefix}0011";
public const string CollectionShouldContainSingle = $"{DiagnosticProperties.IdPrefix}0012";
public const string CollectionShouldOnlyContainProperty = $"{DiagnosticProperties.IdPrefix}0013";
public const string CollectionShouldHaveSameCount = $"{DiagnosticProperties.IdPrefix}0014";
public const string CollectionShouldNotHaveSameCount = $"{DiagnosticProperties.IdPrefix}0015";
public const string CollectionShouldContainSingleProperty = $"{DiagnosticProperties.IdPrefix}0016";
public const string CollectionShouldNotBeNullOrEmpty = $"{DiagnosticProperties.IdPrefix}0017";
public const string CollectionShouldHaveElementAt = $"{DiagnosticProperties.IdPrefix}0018";
public const string CollectionShouldBeInAscendingOrder = $"{DiagnosticProperties.IdPrefix}0019";
public const string CollectionShouldBeInDescendingOrder = $"{DiagnosticProperties.IdPrefix}0020";
public const string CollectionShouldEqualOtherCollectionByComparer = $"{DiagnosticProperties.IdPrefix}0021";
public const string CollectionShouldNotIntersectWith = $"{DiagnosticProperties.IdPrefix}0022";
public const string CollectionShouldIntersectWith = $"{DiagnosticProperties.IdPrefix}0023";
public const string CollectionShouldNotContainNulls = $"{DiagnosticProperties.IdPrefix}0024";
public const string CollectionShouldOnlyHaveUniqueItems = $"{DiagnosticProperties.IdPrefix}0025";
public const string CollectionShouldOnlyHaveUniqueItemsByComparer = $"{DiagnosticProperties.IdPrefix}0026";
public const string CollectionShouldHaveElementAt0Null = $"{DiagnosticProperties.IdPrefix}0027";
}

public static class Dictionaries
Expand Down
239 changes: 229 additions & 10 deletions src/FluentAssertions.Analyzers/Tips/Collections/CollectionAnalyzer.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FluentAssertions.Analyzers.Utilities;
using Microsoft.CodeAnalysis;

namespace FluentAssertions.Analyzers;

public abstract class CollectionBaseAnalyzer : FluentAssertionsAnalyzer
{
protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel)
{
return type.SpecialType != SpecialType.System_String
&& type.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_Generic_IEnumerable_T);
}

protected override bool ShouldAnalyzeVariableType(ITypeSymbol type, SemanticModel semanticModel)
{
return type.SpecialType != SpecialType.System_String
&& type.IsTypeOrConstructedFromTypeOrImplementsType(SpecialType.System_Collections_Generic_IEnumerable_T);
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;

namespace FluentAssertions.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionShouldBeEmptyAnalyzer : CollectionAnalyzer
public static class CollectionShouldBeEmpty
{
public const string DiagnosticId = Constants.Tips.Collections.CollectionsShouldBeEmpty;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().BeEmpty() instead.";

protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
{
get
{
yield return new AnyShouldBeFalseSyntaxVisitor();
yield return new ShouldHaveCount0SyntaxVisitor();
}
}

public class AnyShouldBeFalseSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public AnyShouldBeFalseSyntaxVisitor() : base(MemberValidator.MethodNotContainingLambda("Any"), MemberValidator.Should, new MemberValidator("BeFalse"))
Expand All @@ -49,27 +29,8 @@ private static bool HaveCountArgumentsValidator(SeparatedSyntaxList<ArgumentSynt
&& argument == 0;
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionShouldBeEmptyCodeFix)), Shared]
public class CollectionShouldBeEmptyCodeFix : FluentAssertionsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldBeEmptyAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
{
switch (properties.VisitorName)
{
case nameof(CollectionShouldBeEmptyAnalyzer.AnyShouldBeFalseSyntaxVisitor):
return GetNewExpression(expression, NodeReplacement.Remove("Any"), NodeReplacement.Rename("BeFalse", "BeEmpty"));
case nameof(CollectionShouldBeEmptyAnalyzer.ShouldHaveCount0SyntaxVisitor):
return GetNewExpression(expression, new HaveCountNodeReplacement());
default:
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
}
}

private class HaveCountNodeReplacement : NodeReplacement
public class HaveCountNodeReplacement : NodeReplacement
{
public override bool IsValidNode(LinkedListNode<MemberAccessExpressionSyntax> listNode) => listNode.Value.Name.Identifier.Text == "HaveCount";
public override SyntaxNode ComputeOld(LinkedListNode<MemberAccessExpressionSyntax> listNode) => listNode.Value.Parent;
Expand All @@ -85,4 +46,4 @@ public override SyntaxNode ComputeNew(LinkedListNode<MemberAccessExpressionSynta
return invocation.WithArgumentList(invocation.ArgumentList.WithArguments(arguments));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,11 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
namespace FluentAssertions.Analyzers;

namespace FluentAssertions.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionShouldBeInAscendingOrderAnalyzer : CollectionAnalyzer
public static class CollectionShouldBeInAscendingOrder
{
public const string DiagnosticId = Constants.Tips.Collections.CollectionShouldBeInAscendingOrder;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().BeInAscendingOrder() instead.";

protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
{
get
{
yield return new OrderByShouldEqualSyntaxVisitor();
}
}

public class OrderByShouldEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public OrderByShouldEqualSyntaxVisitor() : base(MemberValidator.MethodContainingLambda("OrderBy"), MemberValidator.Should, MemberValidator.ArgumentIsVariable("Equal"))
{
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionShouldBeInAscendingOrderCodeFix)), Shared]
public class CollectionShouldBeInAscendingOrderCodeFix : FluentAssertionsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldBeInAscendingOrderAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
{
var remove = NodeReplacement.RemoveAndExtractArguments("OrderBy");
var newExpression = GetNewExpression(expression, remove);

newExpression = GetNewExpression(newExpression, NodeReplacement.RenameAndRemoveFirstArgument("Equal", "BeInAscendingOrder"));

return GetNewExpression(newExpression, NodeReplacement.PrependArguments("BeInAscendingOrder", remove.Arguments));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,11 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
namespace FluentAssertions.Analyzers;

namespace FluentAssertions.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionShouldBeInDescendingOrderAnalyzer : CollectionAnalyzer
public static class CollectionShouldBeInDescendingOrder
{
public const string DiagnosticId = Constants.Tips.Collections.CollectionShouldBeInDescendingOrder;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().BeInDescendingOrder() instead.";

protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
{
get
{
yield return new OrderByDescendingShouldEqualSyntaxVisitor();
}
}

public class OrderByDescendingShouldEqualSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public OrderByDescendingShouldEqualSyntaxVisitor() : base(MemberValidator.MethodContainingLambda("OrderByDescending"), MemberValidator.Should, MemberValidator.ArgumentIsVariable("Equal"))
{
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionShouldBeInDescendingOrderCodeFix)), Shared]
public class CollectionShouldBeInDescendingOrderCodeFix : FluentAssertionsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldBeInDescendingOrderAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
{
var remove = NodeReplacement.RemoveAndExtractArguments("OrderByDescending");
var newExpression = GetNewExpression(expression, remove);

newExpression = GetNewExpression(newExpression, NodeReplacement.RenameAndRemoveFirstArgument("Equal", "BeInDescendingOrder"));

return GetNewExpression(newExpression, NodeReplacement.PrependArguments("BeInDescendingOrder", remove.Arguments));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,11 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
namespace FluentAssertions.Analyzers;

namespace FluentAssertions.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionShouldContainItemAnalyzer : CollectionAnalyzer
public static class CollectionShouldContainItem
{
public const string DiagnosticId = Constants.Tips.Collections.CollectionShouldContainItem;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().Contain() instead.";

protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);

protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
{
get
{
yield return new ContainsShouldBeTrueSyntaxVisitor();
}
}

public class ContainsShouldBeTrueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public ContainsShouldBeTrueSyntaxVisitor() : base(new MemberValidator("Contains"), MemberValidator.Should, new MemberValidator("BeTrue"))
{
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionShouldContainItemCodeFix)), Shared]
public class CollectionShouldContainItemCodeFix : FluentAssertionsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldContainItemAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
{
var remove = NodeReplacement.RemoveAndExtractArguments("Contains");
var newExpression = GetNewExpression(expression, remove);

return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeTrue", "Contain", remove.Arguments));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
namespace FluentAssertions.Analyzers;

namespace FluentAssertions.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionShouldContainPropertyAnalyzer : CollectionAnalyzer
public static class CollectionShouldContainProperty
{
public const string DiagnosticId = Constants.Tips.Collections.CollectionShouldContainProperty;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().Contain() instead.";

protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
public class AnyWithLambdaShouldBeTrueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
get
{
yield return new AnyShouldBeTrueSyntaxVisitor();
yield return new WhereShouldNotBeEmptySyntaxVisitor();
}
}

public class AnyShouldBeTrueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public AnyShouldBeTrueSyntaxVisitor() : base(MemberValidator.MethodContainingLambda("Any"), MemberValidator.Should, new MemberValidator("BeTrue"))
public AnyWithLambdaShouldBeTrueSyntaxVisitor() : base(MemberValidator.MethodContainingLambda("Any"), MemberValidator.Should, new MemberValidator("BeTrue"))
{
}
}
Expand All @@ -39,29 +15,4 @@ public class WhereShouldNotBeEmptySyntaxVisitor : FluentAssertionsCSharpSyntaxVi
{
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CollectionShouldContainPropertyCodeFix)), Shared]
public class CollectionShouldContainPropertyCodeFix : FluentAssertionsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldContainPropertyAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
{
if (properties.VisitorName == nameof(CollectionShouldContainPropertyAnalyzer.AnyShouldBeTrueSyntaxVisitor))
{
var remove = NodeReplacement.RemoveAndExtractArguments("Any");
var newExpression = GetNewExpression(expression, remove);

return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeTrue", "Contain", remove.Arguments));
}
else if (properties.VisitorName == nameof(CollectionShouldContainPropertyAnalyzer.WhereShouldNotBeEmptySyntaxVisitor))
{
var remove = NodeReplacement.RemoveAndExtractArguments("Where");
var newExpression = GetNewExpression(expression, remove);

return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("NotBeEmpty", "Contain", remove.Arguments));
}
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
}
}
}
Loading

0 comments on commit dd3a22e

Please sign in to comment.