From cdd9a0145da3fb534b83d3830f3807f30644d439 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Sat, 21 Oct 2017 23:20:31 +0300 Subject: [PATCH] added dictionary analyzers --- .../GenerateCode.cs | 2 +- .../Tips/DictionaryTests.cs | 68 ++++++++++--- .../FluentAssertions.BestPractices.nuspec | 2 +- .../Collections/CollectionShouldHaveCount.cs | 2 +- ...llectionShouldHaveCountGreaterOrEqualTo.cs | 2 +- .../CollectionShouldHaveCountGreaterThan.cs | 2 +- .../CollectionShouldHaveCountLessOrEqualTo.cs | 2 +- .../CollectionShouldHaveCountLessThan.cs | 2 +- .../Collections/CollectionShouldNotBeEmpty.cs | 4 +- .../CollectionShouldNotContainProperty.cs | 6 +- .../CollectionShouldNotHaveCount.cs | 2 +- .../CollectionShouldNotHaveSameCount.cs | 2 +- .../DictionaryShouldContainKeyAndValue.cs | 86 ++++++++++++++-- .../DictionaryShouldContainPair.cs | 98 +++++++++++++++++-- .../BecauseArgumentsSyntaxVisitor.cs | 50 ---------- .../FluentAssertionsCSharpSyntaxVisitor.cs | 14 +++ .../Utilities/NodeReplacement.cs | 36 ++++++- 17 files changed, 280 insertions(+), 100 deletions(-) delete mode 100644 src/FluentAssertions.BestPractices/Utilities/BecauseArgumentsSyntaxVisitor.cs diff --git a/src/FluentAssertions.BestPractices.Tests/GenerateCode.cs b/src/FluentAssertions.BestPractices.Tests/GenerateCode.cs index 67eb5a05..00623fba 100644 --- a/src/FluentAssertions.BestPractices.Tests/GenerateCode.cs +++ b/src/FluentAssertions.BestPractices.Tests/GenerateCode.cs @@ -42,7 +42,7 @@ public static class GenerateCode .AppendLine("{") .AppendLine(" public class TestClass") .AppendLine(" {") - .AppendLine($" public void TestMethod(Dictionary {ActualVariableName}, IDictionary expected, IDictionary unexpected, string expectedKey, TestComplexClass expectedValue, string unexpectedKey, TestComplexClass unexpectedValue)") + .AppendLine($" public void TestMethod(Dictionary {ActualVariableName}, IDictionary expected, IDictionary unexpected, string expectedKey, TestComplexClass expectedValue, string unexpectedKey, TestComplexClass unexpectedValue, KeyValuePair pair)") .AppendLine(" {") .AppendLine($" {assertion}") .AppendLine(" }") diff --git a/src/FluentAssertions.BestPractices.Tests/Tips/DictionaryTests.cs b/src/FluentAssertions.BestPractices.Tests/Tips/DictionaryTests.cs index ae7f09f4..7c0c2db5 100644 --- a/src/FluentAssertions.BestPractices.Tests/Tips/DictionaryTests.cs +++ b/src/FluentAssertions.BestPractices.Tests/Tips/DictionaryTests.cs @@ -71,9 +71,15 @@ public class DictionaryTests public void DictionaryShouldNotContainValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [AssertionDataTestMethod] - [AssertionDiagnostic("actual.Should().ContainKey(expectedKey{0}).And.ContainValue(expectedValue{0});")] - [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expectedKey{0}).And.ContainValue(expectedValue{0}).And.ToString();")] - [NotImplemented] + [AssertionDiagnostic("actual.Should().ContainKey(expectedKey{0}).And.ContainValue(expectedValue);")] + [AssertionDiagnostic("actual.Should().ContainKey(expectedKey).And.ContainValue(expectedValue{0});")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expectedKey{0}).And.ContainValue(expectedValue).And.ToString();")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expectedKey).And.ContainValue(expectedValue{0}).And.ToString();")] + [AssertionDiagnostic("actual.Should().ContainValue(expectedValue{0}).And.ContainKey(expectedKey);")] + [AssertionDiagnostic("actual.Should().ContainValue(expectedValue).And.ContainKey(expectedKey{0});")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(expectedValue{0}).And.ContainKey(expectedKey).And.ToString();")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(expectedValue).And.ContainKey(expectedKey{0}).And.ToString();")] + [Implemented] public void DictionaryShouldContainKeyAndValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); [AssertionDataTestMethod] @@ -89,29 +95,59 @@ public class DictionaryTests [AssertionCodeFix( oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expectedKey).And.ContainValue(expectedValue{0}).And.ToString();", newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(expectedKey, expectedValue{0}).And.ToString();")] - [NotImplemented] + [AssertionCodeFix( + oldAssertion: "actual.Should().ContainValue(expectedValue{0}).And.ContainKey(expectedKey);", + newAssertion: "actual.Should().Contain(expectedKey, expectedValue{0});")] + [AssertionCodeFix( + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(expectedValue).And.ContainKey(expectedKey{0}).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(expectedKey, expectedValue{0}).And.ToString();")] + [AssertionCodeFix( + oldAssertion: "actual.Should().ContainValue(expectedValue).And.ContainKey(expectedKey{0});", + newAssertion: "actual.Should().Contain(expectedKey, expectedValue{0});")] + [AssertionCodeFix( + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(expectedValue{0}).And.ContainKey(expectedKey).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(expectedKey, expectedValue{0}).And.ToString();")] + [Implemented] public void DictionaryShouldContainKeyAndValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [AssertionDataTestMethod] - [AssertionDiagnostic("actual.Should().ContainKey(expected.Key{0}).And.ContainValue(expected.Value);")] - [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expected.Key{0}).And.ContainValue(expected.Value).And.ToString();")] - [NotImplemented] + [AssertionDiagnostic("actual.Should().ContainKey(pair.Key{0}).And.ContainValue(pair.Value);")] + [AssertionDiagnostic("actual.Should().ContainKey(pair.Key).And.ContainValue(pair.Value{0});")] + [AssertionDiagnostic("actual.Should().ContainValue(pair.Value{0}).And.ContainKey(pair.Key);")] + [AssertionDiagnostic("actual.Should().ContainValue(pair.Value).And.ContainKey(pair.Key{0});")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(pair.Key{0}).And.ContainValue(pair.Value).And.ToString();")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(pair.Key).And.ContainValue(pair.Value{0}).And.ToString();")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(pair.Value{0}).And.ContainKey(pair.Key).And.ToString();")] + [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(pair.Value).And.ContainKey(pair.Key{0}).And.ToString();")] + [Implemented] public void DictionaryShouldContainPair_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); [AssertionDataTestMethod] [AssertionCodeFix( - oldAssertion: "actual.Should().ContainKey(expected.Key{0}).And.ContainValue(expected.Value);", - newAssertion: "actual.Should().Contain(expected{0});")] + oldAssertion: "actual.Should().ContainKey(pair.Key{0}).And.ContainValue(pair.Value);", + newAssertion: "actual.Should().Contain(pair{0});")] + [AssertionCodeFix( + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(pair.Key{0}).And.ContainValue(pair.Value).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(pair{0}).And.ToString();")] [AssertionCodeFix( - oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expected.Key{0}).And.ContainValue(expected.Value).And.ToString();", - newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(expected{0}).And.ToString();")] + oldAssertion: "actual.Should().ContainKey(pair.Key).And.ContainValue(pair.Value{0});", + newAssertion: "actual.Should().Contain(pair{0});")] [AssertionCodeFix( - oldAssertion: "actual.Should().ContainKey(expected.Key).And.ContainValue(expected.Value{0});", - newAssertion: "actual.Should().Contain(expected{0});")] + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(pair.Key).And.ContainValue(pair.Value{0}).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(pair{0}).And.ToString();")] [AssertionCodeFix( - oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expected.Key).And.ContainValue(expected.Value{0}).And.ToString();", - newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(expected{0}).And.ToString();")] - [NotImplemented] + oldAssertion: "actual.Should().ContainValue(pair.Value{0}).And.ContainKey(pair.Key);", + newAssertion: "actual.Should().Contain(pair{0});")] + [AssertionCodeFix( + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(pair.Value{0}).And.ContainKey(pair.Key).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(pair{0}).And.ToString();")] + [AssertionCodeFix( + oldAssertion: "actual.Should().ContainValue(pair.Value).And.ContainKey(pair.Key{0});", + newAssertion: "actual.Should().Contain(pair{0});")] + [AssertionCodeFix( + oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(pair.Value).And.ContainKey(pair.Key{0}).And.ToString();", + newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().Contain(pair{0}).And.ToString();")] + [Implemented] public void DictionaryShouldContainPair_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); private void VerifyCSharpDiagnostic(string sourceAssersion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new() diff --git a/src/FluentAssertions.BestPractices/FluentAssertions.BestPractices.nuspec b/src/FluentAssertions.BestPractices/FluentAssertions.BestPractices.nuspec index e0398b1d..c3fa4ef0 100644 --- a/src/FluentAssertions.BestPractices/FluentAssertions.BestPractices.nuspec +++ b/src/FluentAssertions.BestPractices/FluentAssertions.BestPractices.nuspec @@ -3,7 +3,7 @@ FluentAssertions.BestPractices Fluent Assertions Best Practice - 0.4.0 + 0.5.0 Meir Blachman Meir Blachman diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCount.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCount.cs index a4b9b398..e7ae85f0 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCount.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCount.cs @@ -55,7 +55,7 @@ public class CollectionShouldHaveCountCodeFix : FluentAssertionsCodeFixProvider protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("Be", "HaveCount")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("Be", "HaveCount")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterOrEqualTo.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterOrEqualTo.cs index 2dbcf7f6..2b5818aa 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterOrEqualTo.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterOrEqualTo.cs @@ -41,7 +41,7 @@ public class CollectionShouldHaveCountGreaterOrEqualToCodeFix : FluentAssertions protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("BeGreaterOrEqualTo", "HaveCountGreaterOrEqualTo")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("BeGreaterOrEqualTo", "HaveCountGreaterOrEqualTo")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterThan.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterThan.cs index bc1594bc..ac212859 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterThan.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountGreaterThan.cs @@ -42,7 +42,7 @@ public class CollectionShouldHaveCountGreaterThanCodeFix : FluentAssertionsCodeF protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("BeGreaterThan", "HaveCountGreaterThan")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("BeGreaterThan", "HaveCountGreaterThan")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessOrEqualTo.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessOrEqualTo.cs index 27ad2a84..bc58e616 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessOrEqualTo.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessOrEqualTo.cs @@ -41,7 +41,7 @@ public class CollectionShouldHaveCountLessOrEqualToCodeFix : FluentAssertionsCod protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("BeLessOrEqualTo", "HaveCountLessOrEqualTo")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("BeLessOrEqualTo", "HaveCountLessOrEqualTo")); } } } \ No newline at end of file diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessThan.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessThan.cs index 4a24ae51..370ff15b 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessThan.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldHaveCountLessThan.cs @@ -41,7 +41,7 @@ public class CollectionShouldHaveCountLessThanCodeFix : FluentAssertionsCodeFixP protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("BeLessThan", "HaveCountLessThan")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("BeLessThan", "HaveCountLessThan")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotBeEmpty.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotBeEmpty.cs index ae3b0839..d4f2a97b 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotBeEmpty.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotBeEmpty.cs @@ -46,8 +46,8 @@ protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax sta { NodeReplacement[] replacements = { - new NodeReplacement.RemoveNodeReplacement("Any"), - new NodeReplacement.RenameNodeReplacement("BeTrue", "NotBeEmpty") + NodeReplacement.Remove("Any"), + NodeReplacement.Rename("BeTrue", "NotBeEmpty") }; return GetNewStatement(statement, replacements); diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotContainProperty.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotContainProperty.cs index f011efdb..5eb79b1d 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotContainProperty.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotContainProperty.cs @@ -77,18 +77,18 @@ protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax sta var remove = new NodeReplacement.RemoveAndExtractArgumentsNodeReplacement("Any"); var newStatement = GetNewStatement(statement, remove); - return GetNewStatement(newStatement, new NodeReplacement.RenameAndPrependArgumentsNodeReplacement("BeFalse", "NotContain", remove.Arguments)); + return GetNewStatement(newStatement, NodeReplacement.RenameAndPrependArguments("BeFalse", "NotContain", remove.Arguments)); } else if (properties.VisitorName == nameof(CollectionShouldNotContainPropertyAnalyzer.WhereShouldBeEmptySyntaxVisitor)) { var remove = new NodeReplacement.RemoveAndExtractArgumentsNodeReplacement("Where"); var newStatement = GetNewStatement(statement, remove); - return GetNewStatement(newStatement, new NodeReplacement.RenameAndPrependArgumentsNodeReplacement("BeEmpty", "NotContain", remove.Arguments)); + return GetNewStatement(newStatement, NodeReplacement.RenameAndPrependArguments("BeEmpty", "NotContain", remove.Arguments)); } else if (properties.VisitorName == nameof(CollectionShouldNotContainPropertyAnalyzer.ShouldOnlyContainNotSyntaxVisitor)) { - return GetNewStatement(statement, new NodeReplacement.RenameAndNegateLambdaNodeReplacement("OnlyContain", "NotContain")); + return GetNewStatement(statement, NodeReplacement.RenameAndNegateLambda("OnlyContain", "NotContain")); } throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveCount.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveCount.cs index a1a9307d..d860f225 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveCount.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveCount.cs @@ -48,7 +48,7 @@ public class CollectionShouldNotHaveCountCodeFix : FluentAssertionsCodeFixProvid protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameNodeReplacement("NotBe", "NotHaveCount")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), NodeReplacement.Rename("NotBe", "NotHaveCount")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveSameCount.cs b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveSameCount.cs index 0284efa9..8ea4d6bd 100644 --- a/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveSameCount.cs +++ b/src/FluentAssertions.BestPractices/Tips/Collections/CollectionShouldNotHaveSameCount.cs @@ -49,7 +49,7 @@ public class CollectionShouldNotHaveSameCountCodeFix : FluentAssertionsCodeFixPr protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - return GetNewStatement(statement, new NodeReplacement.RemoveNodeReplacement("Count"), new NodeReplacement.RenameAndRemoveInvocationOfMethodOnFirstArgumentNodeReplacement("NotBe", "NotHaveSameCount")); + return GetNewStatement(statement, NodeReplacement.Remove("Count"), new NodeReplacement.RenameAndRemoveInvocationOfMethodOnFirstArgumentNodeReplacement("NotBe", "NotHaveSameCount")); } } } diff --git a/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs b/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs index e46364b4..f2fe9a5c 100644 --- a/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs +++ b/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; @@ -14,23 +15,46 @@ public class DictionaryShouldContainKeyAndValueAnalyzer : FluentAssertionsAnalyz public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainKeyAndValue; public const string Category = Constants.Tips.Category; - public const string Message = "Use {0} .Should() followed by ### instead."; + public const string Message = "Use {0} .Should() followed by .Contain() instead."; protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); protected override IEnumerable Visitors { get { - yield return new DictionaryShouldContainKeyAndValueSyntaxVisitor(); + yield return new ShouldContainKeyAndContainValueSyntaxVisitor(); + yield return new ShouldContainValueAndContainKeySyntaxVisitor(); } } - private class DictionaryShouldContainKeyAndValueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - public DictionaryShouldContainKeyAndValueSyntaxVisitor() : base("###", "Should") - { - } - } + public abstract class ContainKeyValueSyntaxVisitor : FluentAssertionsWithArgumentsCSharpSyntaxVisitor + { + protected override bool AreArgumentsValid() + { + return Arguments.TryGetValue(("ContainKey", 0), out var key) + && (key is IdentifierNameSyntax || key is LiteralExpressionSyntax) + + && Arguments.TryGetValue(("ContainValue", 0), out var value) + && (value is IdentifierNameSyntax || value is LiteralExpressionSyntax); + } + + protected ContainKeyValueSyntaxVisitor(params string[] requiredMethods) : base(requiredMethods) + { + } + } + public class ShouldContainKeyAndContainValueSyntaxVisitor : ContainKeyValueSyntaxVisitor + { + + public ShouldContainKeyAndContainValueSyntaxVisitor() : base("Should", "ContainKey", "And", "ContainValue") + { + } + } + public class ShouldContainValueAndContainKeySyntaxVisitor : ContainKeyValueSyntaxVisitor + { + public ShouldContainValueAndContainKeySyntaxVisitor() : base("Should", "ContainValue", "And", "ContainKey") + { + } + } } [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainKeyAndValueCodeFix)), Shared] @@ -38,9 +62,53 @@ public class DictionaryShouldContainKeyAndValueCodeFix : FluentAssertionsCodeFix { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainKeyAndValueAnalyzer.DiagnosticId); + protected override bool CanRewriteAssertion(ExpressionStatementSyntax statement) + { + var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); + statement.Accept(visitor); + + var containKey = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainKey"); + var containValue = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainValue"); + + return !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1 + && containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1); + } + protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - throw new System.NotImplementedException(); + if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)) + { + var renameKeyArguments = NodeReplacement.RenameAndExtractArguments("ContainKey", "Contain"); + var removeValueArguments = NodeReplacement.RemoveAndExtractArguments("ContainValue"); + var newStatement = GetNewStatement(statement, renameKeyArguments, removeValueArguments); + + var newArguments = MergeContainKeyAndContainValueArguments(renameKeyArguments.Arguments, removeValueArguments.Arguments); + + return GetNewStatement(newStatement, NodeReplacement.WithArguments("Contain", newArguments)); + } + else if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)) + { + var removeKeyArguments = NodeReplacement.RemoveAndExtractArguments("ContainKey"); + var renameValueArguments = NodeReplacement.RenameAndExtractArguments("ContainValue", "Contain"); + var newStatement = GetNewStatement(statement, removeKeyArguments, renameValueArguments); + + var newArguments = MergeContainKeyAndContainValueArguments(removeKeyArguments.Arguments, renameValueArguments.Arguments); + + return GetNewStatement(newStatement, NodeReplacement.WithArguments("Contain", newArguments)); + } + else + { + throw new InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } + + private SeparatedSyntaxList MergeContainKeyAndContainValueArguments(SeparatedSyntaxList keyArguments, SeparatedSyntaxList valueArguments) + { + return new SeparatedSyntaxList() + .Add(keyArguments[0]) + .Add(valueArguments[0]) + .AddRange(keyArguments.RemoveAt(0)) + .AddRange(valueArguments.RemoveAt(0)); } } } \ No newline at end of file diff --git a/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainPair.cs b/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainPair.cs index 388dca4a..509ead57 100644 --- a/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainPair.cs +++ b/src/FluentAssertions.BestPractices/Tips/Dictionaries/DictionaryShouldContainPair.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; @@ -14,23 +15,52 @@ public class DictionaryShouldContainPairAnalyzer : FluentAssertionsAnalyzer public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainPair; public const string Category = Constants.Tips.Category; - public const string Message = "Use {0} .Should() followed by ### instead."; + public const string Message = "Use {0} .Should() followed by .Contain() instead."; protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); protected override IEnumerable Visitors { get { - yield return new DictionaryShouldContainPairSyntaxVisitor(); + yield return new ShouldContainKeyAndContainValueSyntaxVisitor(); + yield return new ShouldContainValueAndContainKeySyntaxVisitor(); } } - private class DictionaryShouldContainPairSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - public DictionaryShouldContainPairSyntaxVisitor() : base("###", "Should") - { - } - } + public abstract class ContainKeyValueSyntaxVisitor : FluentAssertionsWithArgumentsCSharpSyntaxVisitor + { + protected override bool AreArgumentsValid() + { + return Arguments.TryGetValue(("ContainKey", 0), out var key) + && key is MemberAccessExpressionSyntax keyAccess + && keyAccess.Expression is IdentifierNameSyntax keyContainer + && keyAccess.Name.Identifier.Text == "Key" + + && Arguments.TryGetValue(("ContainValue", 0), out var value) + && value is MemberAccessExpressionSyntax valueAccess + && valueAccess.Expression is IdentifierNameSyntax valueContainer + && valueAccess.Name.Identifier.Text == "Value" + + && keyContainer.Identifier.Text == valueContainer.Identifier.Text; + } + + protected ContainKeyValueSyntaxVisitor(params string[] requiredMethods) : base(requiredMethods) + { + } + } + public class ShouldContainKeyAndContainValueSyntaxVisitor : ContainKeyValueSyntaxVisitor + { + + public ShouldContainKeyAndContainValueSyntaxVisitor() : base("Should", "ContainKey", "And", "ContainValue") + { + } + } + public class ShouldContainValueAndContainKeySyntaxVisitor : ContainKeyValueSyntaxVisitor + { + public ShouldContainValueAndContainKeySyntaxVisitor() : base("Should", "ContainValue", "And", "ContainKey") + { + } + } } [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainPairCodeFix)), Shared] @@ -38,9 +68,59 @@ public class DictionaryShouldContainPairCodeFix : FluentAssertionsCodeFixProvide { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainPairAnalyzer.DiagnosticId); + protected override bool CanRewriteAssertion(ExpressionStatementSyntax statement) + { + var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); + statement.Accept(visitor); + + var containKey = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainKey"); + var containValue = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainValue"); + + return !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1 + && containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1); + } + protected override StatementSyntax GetNewStatement(ExpressionStatementSyntax statement, FluentAssertionsDiagnosticProperties properties) { - throw new System.NotImplementedException(); + if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)) + { + var remove = NodeReplacement.RemoveAndExtractArguments("ContainValue"); + var newStatement = GetNewStatement(statement, remove); + + var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); + + newStatement = GetNewStatement(newStatement, NodeReplacement.RenameAndRemoveFirstArgument("ContainKey", "Contain")); + + newStatement = GetNewStatement(newStatement, NodeReplacement.PrependArguments("Contain", newArguments)); + + return newStatement; + } + else if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)) + { + var remove = NodeReplacement.RemoveAndExtractArguments("ContainKey"); + var newStatement = GetNewStatement(statement, remove); + + var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); + + newStatement = GetNewStatement(newStatement, NodeReplacement.RenameAndRemoveFirstArgument("ContainValue", "Contain")); + + newStatement = GetNewStatement(newStatement, NodeReplacement.PrependArguments("Contain", newArguments)); + + return newStatement; + } + else + { + throw new InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); + } + } + + private SeparatedSyntaxList GetArgumentsWithFirstAsPairIdentifierArgument(SeparatedSyntaxList arguments) + { + var argument = arguments[0]; + var memberAccess = (MemberAccessExpressionSyntax)argument.Expression; + var identifier = (IdentifierNameSyntax)memberAccess.Expression; + + return arguments.Replace(argument, argument.WithExpression(identifier)); } } } \ No newline at end of file diff --git a/src/FluentAssertions.BestPractices/Utilities/BecauseArgumentsSyntaxVisitor.cs b/src/FluentAssertions.BestPractices/Utilities/BecauseArgumentsSyntaxVisitor.cs deleted file mode 100644 index 003497e8..00000000 --- a/src/FluentAssertions.BestPractices/Utilities/BecauseArgumentsSyntaxVisitor.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace FluentAssertions.BestPractices -{ - - public sealed class BecauseArgumentsSyntaxVisitor : CSharpSyntaxVisitor - { - private readonly string _methodName; - private readonly int _argumentsStartingIndex; - - public string BecauseArgumentsString { get; private set; } - - public BecauseArgumentsSyntaxVisitor(string methodName, int argumentsStartingIndex) - { - _methodName = methodName; - _argumentsStartingIndex = argumentsStartingIndex; - } - - public override void VisitInvocationExpression(InvocationExpressionSyntax node) - { - base.Visit(node.Expression); - if (BecauseArgumentsString == null) - { - base.Visit(node.ArgumentList); - } - } - - public override void VisitArgumentList(ArgumentListSyntax node) - { - var arguments = node.Arguments; - for (int i = 0; i < _argumentsStartingIndex; i++) - { - arguments = arguments.RemoveAt(0); - } - BecauseArgumentsString = arguments.ToFullString(); - } - - public override void VisitExpressionStatement(ExpressionStatementSyntax node) => base.Visit(node.Expression); - - public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) - { - string methodName = node.Name.Identifier.Text; - if (!methodName.Equals(_methodName)) - { - base.Visit(node.Expression); - } - } - } -} diff --git a/src/FluentAssertions.BestPractices/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs b/src/FluentAssertions.BestPractices/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs index 43df1fb1..5648f386 100644 --- a/src/FluentAssertions.BestPractices/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs +++ b/src/FluentAssertions.BestPractices/Utilities/FluentAssertionsCSharpSyntaxVisitor.cs @@ -58,6 +58,7 @@ public sealed override void VisitMemberAccessExpression(MemberAccessExpressionSy Visit(node.Expression); + if(node.Parent is MemberAccessExpressionSyntax && VisitedMethods.Count > 0) VisitedMethods.Pop(); } public sealed override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) { @@ -87,5 +88,18 @@ private void VisitMethod(string methodName) RequiredMethods.Pop(); } } + +#if DEBUG + private int _indent = 0; + public override void Visit(Microsoft.CodeAnalysis.SyntaxNode node) + { + _indent++; + var indent = new string(' ', _indent * 2); + if(this.GetType().Name == "ShouldContainKeyAndContainValueSyntaxVisitor") + System.Console.WriteLine($"{indent}{CurrentMethod ?? ""}: {node.GetType().Name}"); + base.Visit(node); + --_indent; + } +#endif } } diff --git a/src/FluentAssertions.BestPractices/Utilities/NodeReplacement.cs b/src/FluentAssertions.BestPractices/Utilities/NodeReplacement.cs index 844803b6..3a620f05 100644 --- a/src/FluentAssertions.BestPractices/Utilities/NodeReplacement.cs +++ b/src/FluentAssertions.BestPractices/Utilities/NodeReplacement.cs @@ -19,10 +19,13 @@ public virtual void ExtractValues(MemberAccessExpressionSyntax node) { } public static RemoveAndExtractArgumentsNodeReplacement RemoveAndExtractArguments(string name) => new RemoveAndExtractArgumentsNodeReplacement(name); public static NodeReplacement RenameAndPrependArguments(string oldName, string newName, SeparatedSyntaxList arguments) => new RenameAndPrependArgumentsNodeReplacement(oldName, newName, arguments); public static NodeReplacement RenameAndRemoveInvocationOfMethodOnFirstArgument(string oldName, string newName) => new RenameAndRemoveInvocationOfMethodOnFirstArgumentNodeReplacement(oldName, newName); - public static NodeReplacement RenameAndRemoveFirstArgument(string oldName, string newName) => new RenameAndRemoveFirstArgumentNodeReplacement(oldName, newName); + public static RenameAndRemoveFirstArgumentNodeReplacement RenameAndRemoveFirstArgument(string oldName, string newName) => new RenameAndRemoveFirstArgumentNodeReplacement(oldName, newName); + public static RenameAndExtractArgumentsNodeReplacement RenameAndExtractArguments(string oldName, string newName) => new RenameAndExtractArgumentsNodeReplacement(oldName, newName); public static RemoveFirstArgumentNodeReplacement RemoveFirstArgument(string name) => new RemoveFirstArgumentNodeReplacement(name); public static NodeReplacement PrependArguments(string name, SeparatedSyntaxList arguments) => new PrependArgumentsNodeReplacement(name, arguments); public static RemoveAndRetrieveIndexerArgumentsNodeReplacement RemoveAndRetrieveIndexerArguments(string methodAfterIndexer) => new RemoveAndRetrieveIndexerArgumentsNodeReplacement(methodAfterIndexer); + public static RenameAndNegateLambdaNodeReplacement RenameAndNegateLambda(string oldName, string newName) => new RenameAndNegateLambdaNodeReplacement(oldName, newName); + public static WithArgumentsNodeReplacement WithArguments(string name, SeparatedSyntaxList arguments) => new WithArgumentsNodeReplacement(name, arguments); public class RenameNodeReplacement : NodeReplacement { @@ -152,13 +155,16 @@ public override InvocationExpressionSyntax ComputeNew(InvocationExpressionSyntax public class RenameAndRemoveFirstArgumentNodeReplacement : RenameNodeReplacement { + public ArgumentSyntax Argument { get; private set; } + public RenameAndRemoveFirstArgumentNodeReplacement(string oldName, string newName) : base(oldName, newName) { } public override InvocationExpressionSyntax ComputeNew(InvocationExpressionSyntax node) { - var exceptFirstArgument = node.ArgumentList.Arguments.RemoveAt(0); + Argument = node.ArgumentList.Arguments[0]; + var exceptFirstArgument = node.ArgumentList.Arguments.Remove(Argument); return node.WithArgumentList(node.ArgumentList.WithArguments(exceptFirstArgument)); } @@ -184,6 +190,20 @@ private LambdaExpressionSyntax NagateLambda(LambdaExpressionSyntax lambda) } } + public class RenameAndExtractArgumentsNodeReplacement : RenameNodeReplacement + { + public SeparatedSyntaxList Arguments { get; private set; } + + public RenameAndExtractArgumentsNodeReplacement(string oldName, string newName) : base(oldName, newName) + { + } + + public override void ExtractValues(MemberAccessExpressionSyntax node) + { + Arguments = ((InvocationExpressionSyntax)node.Parent).ArgumentList.Arguments; + } + } + public class RemoveFirstArgumentNodeReplacement : EditNodeReplacement { public ArgumentSyntax Argument { get; private set; } @@ -218,6 +238,18 @@ public override InvocationExpressionSyntax ComputeNew(InvocationExpressionSyntax } } + public class WithArgumentsNodeReplacement : EditNodeReplacement + { + private readonly SeparatedSyntaxList _arguments; + + public WithArgumentsNodeReplacement(string name, SeparatedSyntaxList arguments) : base(name) + { + _arguments = arguments; + } + + public override InvocationExpressionSyntax ComputeNew(InvocationExpressionSyntax node) => node.WithArgumentList(node.ArgumentList.WithArguments(_arguments)); + } + public class RemoveAndRetrieveIndexerArgumentsNodeReplacement : NodeReplacement { private readonly string _methodAfterIndexer;