diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/DictionaryTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/DictionaryTests.cs index 1c6868e2..ef00dfc0 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/DictionaryTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/DictionaryTests.cs @@ -11,7 +11,7 @@ public class DictionaryTests [AssertionDiagnostic("actual.ContainsKey(expectedKey).Should().BeTrue({0});")] [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).ContainsKey(expectedKey).Should().BeTrue({0}).And.ToString();")] [Implemented] - public void DictionaryShouldContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); + public void DictionaryShouldContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainKey_ContainsKeyShouldBeTrue); [DataTestMethod] [AssertionCodeFix( @@ -21,13 +21,13 @@ public class DictionaryTests oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).ContainsKey(expectedKey).Should().BeTrue({0}).And.ToString();", newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainKey(expectedKey{0}).And.ToString();")] [Implemented] - public void DictionaryShouldContainKey_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); + public void DictionaryShouldContainKey_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [DataTestMethod] [AssertionDiagnostic("actual.ContainsKey(expectedKey).Should().BeFalse({0});")] [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).ContainsKey(expectedKey).Should().BeFalse({0}).And.ToString();")] [Implemented] - public void DictionaryShouldNotContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); + public void DictionaryShouldNotContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldNotContainKey_ContainsKeyShouldBeFalse); [DataTestMethod] [AssertionCodeFix( @@ -37,13 +37,13 @@ public class DictionaryTests oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).ContainsKey(expectedKey).Should().BeFalse({0}).And.ToString();", newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().NotContainKey(expectedKey{0}).And.ToString();")] [Implemented] - public void DictionaryShouldNotContainKey_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); + public void DictionaryShouldNotContainKey_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [DataTestMethod] [AssertionDiagnostic("actual.ContainsValue(expectedValue).Should().BeTrue({0});")] [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).ContainsValue(expectedValue).Should().BeTrue({0}).And.ToString();")] [Implemented] - public void DictionaryShouldContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); + public void DictionaryShouldContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainValue_ContainsValueShouldBeTrue); [DataTestMethod] [AssertionCodeFix( @@ -53,13 +53,13 @@ public class DictionaryTests oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).ContainsValue(expectedValue).Should().BeTrue({0}).And.ToString();", newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().ContainValue(expectedValue{0}).And.ToString();")] [Implemented] - public void DictionaryShouldContainValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); + public void DictionaryShouldContainValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [DataTestMethod] [AssertionDiagnostic("actual.ContainsValue(expectedValue).Should().BeFalse({0});")] [AssertionDiagnostic("actual.ToDictionary(p => p.Key, p=> p.Value).ContainsValue(expectedValue).Should().BeFalse({0}).And.ToString();")] [Implemented] - public void DictionaryShouldNotContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion); + public void DictionaryShouldNotContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldNotContainValue_ContainsValueShouldBeFalse); [DataTestMethod] [AssertionCodeFix( @@ -69,19 +69,23 @@ public class DictionaryTests oldAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).ContainsValue(expectedValue).Should().BeFalse({0}).And.ToString();", newAssertion: "actual.ToDictionary(p => p.Key, p=> p.Value).Should().NotContainValue(expectedValue{0}).And.ToString();")] [Implemented] - public void DictionaryShouldNotContainValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); + public void DictionaryShouldNotContainValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [DataTestMethod] [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();")] + [Implemented] + public void DictionaryShouldContainKeyAndValue_ShouldContainKeyAndContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainKeyAndContainValue); + + [DataTestMethod] [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); + public void DictionaryShouldContainKeyAndValue_ShouldContainValueAndContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainValueAndContainKey); [DataTestMethod] [AssertionCodeFix( @@ -109,19 +113,23 @@ public class DictionaryTests 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); + public void DictionaryShouldContainKeyAndValue_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); [DataTestMethod] [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();")] + [Implemented] + public void DictionaryShouldContainPair_ShouldContainKeyAndContainValue_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainKeyAndContainValue); + + [DataTestMethod] + [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().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); + public void DictionaryShouldContainPair_ShouldContainValueAndContainKey_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainValueAndContainKey); [DataTestMethod] [AssertionCodeFix( @@ -149,20 +157,17 @@ public class DictionaryTests 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); + public void DictionaryShouldContainPair_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); - private void VerifyCSharpDiagnostic(string sourceAssersion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new() + private void VerifyCSharpDiagnostic(string sourceAssersion, DiagnosticMetadata metadata) { var source = GenerateCode.GenericIDictionaryAssertion(sourceAssersion); - var type = typeof(TDiagnosticAnalyzer); - var diagnosticId = (string)type.GetField("DiagnosticId").GetValue(null); - var message = (string)type.GetField("Message").GetValue(null); - DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult { - Id = diagnosticId, - Message = message, + Id = FluentAssertionsOperationAnalyzer.DiagnosticId, + Message = metadata.Message, + VisitorName = metadata.Name, Locations = new DiagnosticResultLocation[] { new DiagnosticResultLocation("Test0.cs", 12,13) diff --git a/src/FluentAssertions.Analyzers/Tips/DiagnosticMetadata.cs b/src/FluentAssertions.Analyzers/Tips/DiagnosticMetadata.cs index d139afdf..33d4de45 100644 --- a/src/FluentAssertions.Analyzers/Tips/DiagnosticMetadata.cs +++ b/src/FluentAssertions.Analyzers/Tips/DiagnosticMetadata.cs @@ -70,5 +70,14 @@ private DiagnosticMetadata(string message, string helpLink, [CallerMemberName] s public static DiagnosticMetadata StringShouldNotBeNullOrWhiteSpace_StringShouldNotBeNullOrWhiteSpace { get; } = new("Use .Should().NotBeNullOrWhiteSpace()", GetHelpLink("Strings-7")); public static DiagnosticMetadata StringShouldHaveLength_LengthShouldBe { get; } = new("Use .Should().HaveLength()", GetHelpLink("Strings-8")); + public static DiagnosticMetadata DictionaryShouldContainKey_ContainsKeyShouldBeTrue = new("Use .Should().ContainKey()", GetHelpLink("Dictionaries-1")); + public static DiagnosticMetadata DictionaryShouldNotContainKey_ContainsKeyShouldBeFalse = new("Use .Should().NotContainKey() ", GetHelpLink("Dictionaries-2")); + public static DiagnosticMetadata DictionaryShouldContainValue_ContainsValueShouldBeTrue = new("Use .Should().ContainValue() ", GetHelpLink("Dictionaries-3")); + public static DiagnosticMetadata DictionaryShouldNotContainValue_ContainsValueShouldBeFalse = new("Use .Should().NotContainValue() ", GetHelpLink("Dictionaries-4")); + public static DiagnosticMetadata DictionaryShouldContainKeyAndValue_ShouldContainKeyAndContainValue = new("Use .Should().Contain() ", GetHelpLink("Dictionaries-5")); + public static DiagnosticMetadata DictionaryShouldContainKeyAndValue_ShouldContainValueAndContainKey = new("Use .Should().Contain() ", GetHelpLink("Dictionaries-5")); + public static DiagnosticMetadata DictionaryShouldContainPair_ShouldContainKeyAndContainValue = new("Use .Should().Contain() ", GetHelpLink("Dictionaries-6")); + public static DiagnosticMetadata DictionaryShouldContainPair_ShouldContainValueAndContainKey = new("Use .Should().Contain() ", GetHelpLink("Dictionaries-6")); + private static string GetHelpLink(string id) => $"https://fluentassertions.com/tips/#{id}"; } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryAnalyzer.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryAnalyzer.cs deleted file mode 100644 index 3a43cbae..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryAnalyzer.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentAssertions.Analyzers.Utilities; -using Microsoft.CodeAnalysis; - -namespace FluentAssertions.Analyzers; - -public abstract class DictionaryAnalyzer : FluentAssertionsAnalyzer -{ - protected override bool ShouldAnalyzeVariableNamedType(INamedTypeSymbol type, SemanticModel semanticModel) - { - var iDictionaryType = semanticModel.GetGenericIDictionaryType(); - return type.IsTypeOrConstructedFromTypeOrImplementsType(iDictionaryType); - } -} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKey.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKey.cs deleted file mode 100644 index ff1ef1fd..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKey.cs +++ /dev/null @@ -1,48 +0,0 @@ -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; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldContainKeyAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainKey; - public const string Category = Constants.Tips.Category; - - public const string Message = "Use .Should().ContainKey() instead."; - - protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); - protected override IEnumerable Visitors - { - get - { - yield return new ContainsKeyShouldBeTrueSyntaxVisitor(); - } - } - - public class ContainsKeyShouldBeTrueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - public ContainsKeyShouldBeTrueSyntaxVisitor() : base(new MemberValidator("ContainsKey"), MemberValidator.Should, new MemberValidator("BeTrue")) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainKeyCodeFix)), Shared] -public class DictionaryShouldContainKeyCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainKeyAnalyzer.DiagnosticId); - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainsKey"); - var newExpression = GetNewExpression(expression, remove); - - return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeTrue", "ContainKey", remove.Arguments)); - } -} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs deleted file mode 100644 index 6d544fe3..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.CodeAnalysis; -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; -using System.Threading.Tasks; - -namespace FluentAssertions.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldContainKeyAndValueAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainKeyAndValue; - 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 Visitors - { - get - { - yield return new ShouldContainKeyAndContainValueSyntaxVisitor(); - yield return new ShouldContainValueAndContainKeySyntaxVisitor(); - } - } - - public class ShouldContainKeyAndContainValueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - - public ShouldContainKeyAndContainValueSyntaxVisitor() : base(MemberValidator.Should, MemberValidator.ArgumentIsIdentifierOrLiteral("ContainKey"), MemberValidator.And, MemberValidator.ArgumentIsIdentifierOrLiteral("ContainValue")) - { - } - } - - public class ShouldContainValueAndContainKeySyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - public ShouldContainValueAndContainKeySyntaxVisitor() : base(MemberValidator.Should, MemberValidator.ArgumentIsIdentifierOrLiteral("ContainValue"), MemberValidator.And, MemberValidator.ArgumentIsIdentifierOrLiteral("ContainKey")) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainKeyAndValueCodeFix)), Shared] -public class DictionaryShouldContainKeyAndValueCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainKeyAndValueAnalyzer.DiagnosticId); - - protected override Task CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context) - { - var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); - expression.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 Task.FromResult( - !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1 - && containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1) - ); - } - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)) - { - var renameKeyArguments = NodeReplacement.RenameAndExtractArguments("ContainKey", "Contain"); - var removeValueArguments = NodeReplacement.RemoveAndExtractArguments("ContainValue"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainValue"), renameKeyArguments, removeValueArguments); - - var newArguments = MergeContainKeyAndContainValueArguments(renameKeyArguments.Arguments, removeValueArguments.Arguments); - - return GetNewExpression(newExpression, NodeReplacement.WithArguments("Contain", newArguments)); - } - else if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)) - { - var removeKeyArguments = NodeReplacement.RemoveAndExtractArguments("ContainKey"); - var renameValueArguments = NodeReplacement.RenameAndExtractArguments("ContainValue", "Contain"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainKey"), removeKeyArguments, renameValueArguments); - - var newArguments = MergeContainKeyAndContainValueArguments(removeKeyArguments.Arguments, renameValueArguments.Arguments); - - return GetNewExpression(newExpression, 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.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs deleted file mode 100644 index 1bacc749..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs +++ /dev/null @@ -1,156 +0,0 @@ -using Microsoft.CodeAnalysis; -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; -using System.Threading.Tasks; - -namespace FluentAssertions.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldContainPairAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainPair; - 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 Visitors - { - get - { - yield return new ShouldContainKeyAndContainValueSyntaxVisitor(); - yield return new ShouldContainValueAndContainKeySyntaxVisitor(); - } - } - - public abstract class ContainKeyValueSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - protected ContainKeyValueSyntaxVisitor(params MemberValidator[] members) : base(members) - { - } - - public override bool IsValid(ExpressionSyntax expression) - { - if (!base.IsValid(expression)) return false; - - var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); - expression.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 keyInvocation - && containValue.Parent is InvocationExpressionSyntax valueInvocation - - && keyInvocation.ArgumentList.Arguments is SeparatedSyntaxList containKeyArguments - && valueInvocation.ArgumentList.Arguments is SeparatedSyntaxList containValueArguments - - && containKeyArguments.First().Expression is MemberAccessExpressionSyntax keyArgument - && containValueArguments.First().Expression is MemberAccessExpressionSyntax valueArgument - - && keyArgument.Expression is IdentifierNameSyntax keyIdentifier - && valueArgument.Expression is IdentifierNameSyntax valueIdentifier - - && keyIdentifier.Identifier.Text == valueIdentifier.Identifier.Text; - } - - protected static bool KeyIsProperty(SeparatedSyntaxList arguments, SemanticModel semanticModel) - { - if (!arguments.Any()) return false; - - return arguments.First().Expression is MemberAccessExpressionSyntax valueAccess - && valueAccess.Expression is IdentifierNameSyntax identifier - && valueAccess.Name.Identifier.Text == "Key"; - } - protected static bool ValueIsProperty(SeparatedSyntaxList arguments, SemanticModel semanticModel) - { - if (!arguments.Any()) return false; - - return arguments.First().Expression is MemberAccessExpressionSyntax valueAccess - && valueAccess.Expression is IdentifierNameSyntax identifier - && valueAccess.Name.Identifier.Text == "Value"; - } - } - - public class ShouldContainKeyAndContainValueSyntaxVisitor : ContainKeyValueSyntaxVisitor - { - public ShouldContainKeyAndContainValueSyntaxVisitor() : base(MemberValidator.Should, new MemberValidator("ContainKey", KeyIsProperty), MemberValidator.And, new MemberValidator("ContainValue", ValueIsProperty)) - { - } - } - - public class ShouldContainValueAndContainKeySyntaxVisitor : ContainKeyValueSyntaxVisitor - { - public ShouldContainValueAndContainKeySyntaxVisitor() : base(MemberValidator.Should, new MemberValidator("ContainValue", ValueIsProperty), MemberValidator.And, new MemberValidator("ContainKey", KeyIsProperty)) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainPairCodeFix)), Shared] -public class DictionaryShouldContainPairCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainPairAnalyzer.DiagnosticId); - - protected override Task CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context) - { - var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor(); - expression.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 Task.FromResult( - !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1 - && containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1) - ); - } - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainValue"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainValue"), remove); - - var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); - - newExpression = GetNewExpression(newExpression, NodeReplacement.RenameAndRemoveFirstArgument("ContainKey", "Contain")); - - newExpression = GetNewExpression(newExpression, NodeReplacement.PrependArguments("Contain", newArguments)); - - return newExpression; - } - else if (properties.VisitorName == nameof(DictionaryShouldContainPairAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainKey"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainKey"), remove); - - var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); - - newExpression = GetNewExpression(newExpression, NodeReplacement.RenameAndRemoveFirstArgument("ContainValue", "Contain")); - - newExpression = GetNewExpression(newExpression, NodeReplacement.PrependArguments("Contain", newArguments)); - - return newExpression; - } - 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)); - } -} diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainValue.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainValue.cs deleted file mode 100644 index aa4be4e9..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainValue.cs +++ /dev/null @@ -1,48 +0,0 @@ -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; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldContainValueAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldContainValue; - public const string Category = Constants.Tips.Category; - - public const string Message = "Use .Should().ContainValue() instead."; - - protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); - protected override IEnumerable Visitors - { - get - { - yield return new ContainsValueShouldBeTrueSyntaxVisitor(); - } - } - - public class ContainsValueShouldBeTrueSyntaxVisitor: FluentAssertionsCSharpSyntaxVisitor - { - public ContainsValueShouldBeTrueSyntaxVisitor() : base(new MemberValidator("ContainsValue"), MemberValidator.Should, new MemberValidator("BeTrue")) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldContainValueCodeFix)), Shared] -public class DictionaryShouldContainValueCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainValueAnalyzer.DiagnosticId); - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainsValue"); - var newExpression = GetNewExpression(expression, remove); - - return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeTrue", "ContainValue", remove.Arguments)); - } -} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainKey.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainKey.cs deleted file mode 100644 index 2c876615..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainKey.cs +++ /dev/null @@ -1,48 +0,0 @@ -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; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldNotContainKeyAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldNotContainKey; - public const string Category = Constants.Tips.Category; - - public const string Message = "Use .Should().NotContainKey() instead."; - - protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); - protected override IEnumerable Visitors - { - get - { - yield return new ContainsKeyShouldBeFalseSyntaxVisitor(); - } - } - - public class ContainsKeyShouldBeFalseSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor - { - public ContainsKeyShouldBeFalseSyntaxVisitor() : base(new MemberValidator("ContainsKey"), MemberValidator.Should, new MemberValidator("BeFalse")) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldNotContainKeyCodeFix)), Shared] -public class DictionaryShouldNotContainKeyCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldNotContainKeyAnalyzer.DiagnosticId); - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainsKey"); - var newExpression = GetNewExpression(expression, remove); - - return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeFalse", "NotContainKey", remove.Arguments)); - } -} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainValue.cs b/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainValue.cs deleted file mode 100644 index 06e616c7..00000000 --- a/src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldNotContainValue.cs +++ /dev/null @@ -1,48 +0,0 @@ -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; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class DictionaryShouldNotContainValueAnalyzer : DictionaryAnalyzer -{ - public const string DiagnosticId = Constants.Tips.Dictionaries.DictionaryShouldNotContainValue; - public const string Category = Constants.Tips.Category; - - public const string Message = "Use .Should().NotContainValue() instead."; - - protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true); - protected override IEnumerable Visitors - { - get - { - yield return new ContainsValueShouldBeFalseSyntaxVisitor(); - } - } - - public class ContainsValueShouldBeFalseSyntaxVisitor: FluentAssertionsCSharpSyntaxVisitor - { - public ContainsValueShouldBeFalseSyntaxVisitor() : base(new MemberValidator("ContainsValue"), MemberValidator.Should, new MemberValidator("BeFalse")) - { - } - } -} - -[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DictionaryShouldNotContainValueCodeFix)), Shared] -public class DictionaryShouldNotContainValueCodeFix : FluentAssertionsCodeFixProvider -{ - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldNotContainValueAnalyzer.DiagnosticId); - - protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) - { - var remove = NodeReplacement.RemoveAndExtractArguments("ContainsValue"); - var newExpression = GetNewExpression(expression, remove); - - return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeFalse", "NotContainValue", remove.Arguments)); - } -} \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.DictionaryShouldContainPair.cs b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.DictionaryShouldContainPair.cs new file mode 100644 index 00000000..1a1a5779 --- /dev/null +++ b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.DictionaryShouldContainPair.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace FluentAssertions.Analyzers; + +public partial class FluentAssertionsCodeFix +{ + 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.Analyzers/Tips/FluentAssertionsCodeFix.cs b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.cs index 563e3b87..dfa52e20 100644 --- a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.cs +++ b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.cs @@ -49,6 +49,38 @@ ExpressionSyntax GetCombinedAssertions(string remove, string rename, string newN return GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments(rename, newName, removeNode.Arguments)); } + // oldAssertion1: subject.Should().(arg1, {reasonArgs1}).And.(arg2); + // oldAssertion2: subject.Should().(arg1).And.(arg2, {reasonArgs}); + // newAssertion : subject.Should().(arg1, arg2, {reasonArgs}); + ExpressionSyntax GetCombinedAssertionsWithArguments(string remove, string rename, string newName) + { + var removeNode = NodeReplacement.RemoveAndExtractArguments(remove); + var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore(remove), removeNode); + + var renameNode = NodeReplacement.RenameAndExtractArguments(rename, newName); + newExpression = GetNewExpression(newExpression, renameNode); + + var arguments = renameNode.Arguments.InsertRange(1, removeNode.Arguments); + + return GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, arguments)); + } + + // oldAssertion1: subject.Should().(arg1, {reasonArgs1}).And.(arg2); + // oldAssertion2: subject.Should().(arg1).And.(arg2, {reasonArgs}); + // newAssertion : subject.Should().(arg2, arg1, {reasonArgs}); + ExpressionSyntax GetCombinedAssertionsWithArgumentsReversedOrder(string remove, string rename, string newName) + { + var removeNode = NodeReplacement.RemoveAndExtractArguments(remove); + var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore(remove), removeNode); + + var renameNode = NodeReplacement.RenameAndExtractArguments(rename, newName); + newExpression = GetNewExpression(newExpression, renameNode); + + var arguments = removeNode.Arguments.InsertRange(1, renameNode.Arguments); + + return GetNewExpression(newExpression, NodeReplacement.WithArguments(newName, arguments)); + } + switch (properties.VisitorName) { case nameof(DiagnosticMetadata.CollectionShouldBeEmpty_AnyShouldBeFalse): @@ -155,29 +187,9 @@ ExpressionSyntax GetCombinedAssertions(string remove, string rename, string newN case nameof(DiagnosticMetadata.NumericShouldBeNegative_ShouldBeLessThan): return GetNewExpression(expression, NodeReplacement.RenameAndRemoveFirstArgument("BeLessThan", "BeNegative")); case nameof(DiagnosticMetadata.NumericShouldBeInRange_BeGreaterOrEqualToAndBeLessOrEqualTo): - { - var removeLess = NodeReplacement.RemoveAndExtractArguments("BeLessOrEqualTo"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("BeLessOrEqualTo"), removeLess); - - var renameGreater = NodeReplacement.RenameAndExtractArguments("BeGreaterOrEqualTo", "BeInRange"); - newExpression = GetNewExpression(newExpression, renameGreater); - - var arguments = renameGreater.Arguments.InsertRange(1, removeLess.Arguments); - - return GetNewExpression(newExpression, NodeReplacement.WithArguments("BeInRange", arguments)); - } + return GetCombinedAssertionsWithArguments(remove: "BeLessOrEqualTo", rename: "BeGreaterOrEqualTo", newName: "BeInRange"); case nameof(DiagnosticMetadata.NumericShouldBeInRange_BeLessOrEqualToAndBeGreaterOrEqualTo): - { - var removeGreater = NodeReplacement.RemoveAndExtractArguments("BeGreaterOrEqualTo"); - var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("BeGreaterOrEqualTo"), removeGreater); - - var renameLess = NodeReplacement.RenameAndExtractArguments("BeLessOrEqualTo", "BeInRange"); - newExpression = GetNewExpression(newExpression, renameLess); - - var arguments = removeGreater.Arguments.InsertRange(1, renameLess.Arguments); - - return GetNewExpression(newExpression, NodeReplacement.WithArguments("BeInRange", arguments)); - } + return GetCombinedAssertionsWithArguments(remove: "BeGreaterOrEqualTo", rename: "BeLessOrEqualTo", newName: "BeInRange"); case nameof(DiagnosticMetadata.NumericShouldBeApproximately_MathAbsShouldBeLessOrEqualTo): { var remove = NodeReplacement.RemoveAndExtractArguments("Abs"); @@ -249,11 +261,46 @@ ExpressionSyntax GetCombinedAssertions(string remove, string rename, string newN return newExpression.ReplaceNode(stringKeyword, subject.WithTriviaFrom(stringKeyword)); } case nameof(DiagnosticMetadata.StringShouldHaveLength_LengthShouldBe): - return GetNewExpression(expression, - NodeReplacement.Remove("Length"), + return GetNewExpression(expression, + NodeReplacement.Remove("Length"), NodeReplacement.Rename("Be", "HaveLength") ); + case nameof(DiagnosticMetadata.DictionaryShouldContainKey_ContainsKeyShouldBeTrue): + return RemoveAndRenameWithArgumentsFromRemoved(remove: "ContainsKey", rename: "BeTrue", newName: "ContainKey"); + case nameof(DiagnosticMetadata.DictionaryShouldNotContainKey_ContainsKeyShouldBeFalse): + return RemoveAndRenameWithArgumentsFromRemoved(remove: "ContainsKey", rename: "BeFalse", newName: "NotContainKey"); + case nameof(DiagnosticMetadata.DictionaryShouldContainValue_ContainsValueShouldBeTrue): + return RemoveAndRenameWithArgumentsFromRemoved(remove: "ContainsValue", rename: "BeTrue", newName: "ContainValue"); + case nameof(DiagnosticMetadata.DictionaryShouldNotContainValue_ContainsValueShouldBeFalse): + return RemoveAndRenameWithArgumentsFromRemoved(remove: "ContainsValue", rename: "BeFalse", newName: "NotContainValue"); + case nameof(DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainKeyAndContainValue): + return GetCombinedAssertionsWithArguments(remove: "ContainValue", rename: "ContainKey", newName: "Contain"); + case nameof(DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainValueAndContainKey): + return GetCombinedAssertionsWithArgumentsReversedOrder(remove: "ContainKey", rename: "ContainValue", newName: "Contain"); + case nameof(DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainKeyAndContainValue): + { + var remove = NodeReplacement.RemoveAndExtractArguments("ContainValue"); + var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainValue"), remove); + + var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); + + return GetNewExpression(newExpression, + NodeReplacement.RenameAndRemoveFirstArgument("ContainKey", "Contain"), + NodeReplacement.PrependArguments("Contain", newArguments) + ); + } + case nameof(DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainValueAndContainKey): + { + var remove = NodeReplacement.RemoveAndExtractArguments("ContainKey"); + var newExpression = GetNewExpression(expression, NodeReplacement.RemoveMethodBefore("ContainKey"), remove); + var newArguments = GetArgumentsWithFirstAsPairIdentifierArgument(remove.Arguments); + + return GetNewExpression(newExpression, + NodeReplacement.RenameAndRemoveFirstArgument("ContainValue", "Contain"), + NodeReplacement.PrependArguments("Contain", newArguments) + ); + } default: throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}"); }; } diff --git a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.Utils.cs b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.Utils.cs index 50e78028..d237a7e7 100644 --- a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.Utils.cs +++ b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.Utils.cs @@ -25,10 +25,11 @@ public FluentAssertionsMetadata(Compilation compilation) NumericAssertionsOfT2 = compilation.GetTypeByMetadataName("FluentAssertions.Numeric.NumericAssertions`2"); BooleanAssertionsOfT1 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.BooleanAssertions`1"); GenericCollectionAssertionsOfT3 = compilation.GetTypeByMetadataName("FluentAssertions.Collections.GenericCollectionAssertions`3"); + GenericDictionaryAssertionsOfT4 = compilation.GetTypeByMetadataName("FluentAssertions.Collections.GenericDictionaryAssertions`4"); StringAssertionsOfT1 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.StringAssertions`1"); IDictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(IDictionary<,>).FullName); + DictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); IReadonlyDictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(IReadOnlyDictionary<,>).FullName); - Enumerable = compilation.GetTypeByMetadataName(typeof(Enumerable).FullName); Math = compilation.GetTypeByMetadataName(typeof(Math).FullName); } @@ -36,8 +37,10 @@ public FluentAssertionsMetadata(Compilation compilation) public INamedTypeSymbol ReferenceTypeAssertionsOfT2 { get; } public INamedTypeSymbol ObjectAssertionsOfT2 { get; } public INamedTypeSymbol GenericCollectionAssertionsOfT3 { get; } + public INamedTypeSymbol GenericDictionaryAssertionsOfT4 { get; } public INamedTypeSymbol StringAssertionsOfT1 { get; } public INamedTypeSymbol IDictionaryOfT2 { get; } + public INamedTypeSymbol DictionaryOfT2 { get; } public INamedTypeSymbol IReadonlyDictionaryOfT2 { get; } public INamedTypeSymbol BooleanAssertionsOfT1 { get; } public INamedTypeSymbol NumericAssertionsOfT2 { get; } diff --git a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.cs b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.cs index b6378ac3..ed96a53a 100644 --- a/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.cs +++ b/src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.cs @@ -179,6 +179,12 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, FluentAs case nameof(string.StartsWith) when invocationBeforeShould.IsContainedInType(SpecialType.System_String): context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.StringShouldStartWith_StartsWithShouldBeTrue)); return; + case nameof(IDictionary.ContainsKey) when invocationBeforeShould.ImplementsOrIsInterface(metadata.IDictionaryOfT2) || invocationBeforeShould.ImplementsOrIsInterface(metadata.IReadonlyDictionaryOfT2): + context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainKey_ContainsKeyShouldBeTrue)); + return; + case nameof(Dictionary.ContainsValue) when invocationBeforeShould.IsContainedInType(metadata.DictionaryOfT2): + context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldContainValue_ContainsValueShouldBeTrue)); + return; } } if (subject is IInvocationOperation shouldArgumentInvocation) @@ -211,6 +217,12 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, FluentAs case nameof(ICollection.Contains) when invocationBeforeShould.ImplementsOrIsInterface(SpecialType.System_Collections_Generic_ICollection_T) && invocationBeforeShould.Arguments.Length == 1: context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldNotContainItem_ContainsShouldBeFalse)); return; + case nameof(IDictionary.ContainsKey) when invocationBeforeShould.ImplementsOrIsInterface(metadata.IDictionaryOfT2) || invocationBeforeShould.ImplementsOrIsInterface(metadata.IReadonlyDictionaryOfT2): + context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldNotContainKey_ContainsKeyShouldBeFalse)); + return; + case nameof(Dictionary.ContainsValue) when invocationBeforeShould.IsContainedInType(metadata.DictionaryOfT2): + context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.DictionaryShouldNotContainValue_ContainsValueShouldBeFalse)); + return; } } if (subject is IInvocationOperation shouldArgumentInvocation) @@ -446,6 +458,56 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, FluentAs } } return; + case "ContainKey" when assertion.IsContainedInType(metadata.GenericDictionaryAssertionsOfT4): + { + if (assertion.TryGetChainedInvocationAfterAndConstraint("ContainValue", out var chainedInvocation)) + { + if (!assertion.HasEmptyBecauseAndReasonArgs(startingIndex: 1) && !chainedInvocation.HasEmptyBecauseAndReasonArgs(startingIndex: 1)) return; + + if (assertion.Arguments[0].Value is IPropertyReferenceOperation { Property.Name: nameof(KeyValuePair.Key) } firstPropertyReference + && chainedInvocation.Arguments[0].Value is IPropertyReferenceOperation { Property.Name: nameof(KeyValuePair.Value) } secondPropertyReference) + { + if (firstPropertyReference.IsSamePropertyReference(secondPropertyReference)) + { + context.ReportDiagnostic(CreateDiagnostic(chainedInvocation, DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainKeyAndContainValue)); + return; + } + // TODO: report here + } + else + { + context.ReportDiagnostic(CreateDiagnostic(chainedInvocation, DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainKeyAndContainValue)); + return; + } + return; + } + } + return; + case "ContainValue" when assertion.IsContainedInType(metadata.GenericDictionaryAssertionsOfT4): + { + if (assertion.TryGetChainedInvocationAfterAndConstraint("ContainKey", out var chainedInvocation)) + { + if (!assertion.HasEmptyBecauseAndReasonArgs(startingIndex: 1) && !chainedInvocation.HasEmptyBecauseAndReasonArgs(startingIndex: 1)) return; + + if (assertion.Arguments[0].Value is IPropertyReferenceOperation { Property.Name: nameof(KeyValuePair.Value) } firstPropertyReference + && chainedInvocation.Arguments[0].Value is IPropertyReferenceOperation { Property.Name: nameof(KeyValuePair.Key) } secondPropertyReference) + { + if (firstPropertyReference.IsSamePropertyReference(secondPropertyReference)) + { + context.ReportDiagnostic(CreateDiagnostic(chainedInvocation, DiagnosticMetadata.DictionaryShouldContainPair_ShouldContainValueAndContainKey)); + return; + } + // TODO: report here + } + else + { + context.ReportDiagnostic(CreateDiagnostic(chainedInvocation, DiagnosticMetadata.DictionaryShouldContainKeyAndValue_ShouldContainValueAndContainKey)); + return; + } + return; + } + } + return; } } diff --git a/src/FluentAssertions.Analyzers/Utilities/HelpLinks.cs b/src/FluentAssertions.Analyzers/Utilities/HelpLinks.cs index c6942f2d..12129c5f 100644 --- a/src/FluentAssertions.Analyzers/Utilities/HelpLinks.cs +++ b/src/FluentAssertions.Analyzers/Utilities/HelpLinks.cs @@ -12,15 +12,6 @@ static HelpLinks() { TypesHelpLinks = new Dictionary { - [typeof(DictionaryShouldContainKeyAnalyzer.ContainsKeyShouldBeTrueSyntaxVisitor)] = GetHelpLink("Dictionaries-1"), - [typeof(DictionaryShouldNotContainKeyAnalyzer.ContainsKeyShouldBeFalseSyntaxVisitor)] = GetHelpLink("Dictionaries-2"), - [typeof(DictionaryShouldContainValueAnalyzer.ContainsValueShouldBeTrueSyntaxVisitor)] = GetHelpLink("Dictionaries-3"), - [typeof(DictionaryShouldNotContainValueAnalyzer.ContainsValueShouldBeFalseSyntaxVisitor)] = GetHelpLink("Dictionaries-4"), - [typeof(DictionaryShouldContainKeyAndValueAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)] = GetHelpLink("Dictionaries-5"), - [typeof(DictionaryShouldContainKeyAndValueAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)] = GetHelpLink("Dictionaries-5"), - [typeof(DictionaryShouldContainPairAnalyzer.ShouldContainKeyAndContainValueSyntaxVisitor)] = GetHelpLink("Dictionaries-6"), - [typeof(DictionaryShouldContainPairAnalyzer.ShouldContainValueAndContainKeySyntaxVisitor)] = GetHelpLink("Dictionaries-6"), - [typeof(ExceptionShouldThrowWithMessageAnalyzer.ShouldThrowExactlyWhichMessageShouldContain)] = GetHelpLink("Exceptions-1"), [typeof(ExceptionShouldThrowWithMessageAnalyzer.ShouldThrowExactlyAndMessageShouldContain)] = GetHelpLink("Exceptions-1"), [typeof(ExceptionShouldThrowWithMessageAnalyzer.ShouldThrowWhichMessageShouldContain)] = GetHelpLink("Exceptions-2"), diff --git a/src/FluentAssertions.Analyzers/Utilities/OperartionExtensions.cs b/src/FluentAssertions.Analyzers/Utilities/OperartionExtensions.cs index 3f0d08d4..384f6a4e 100644 --- a/src/FluentAssertions.Analyzers/Utilities/OperartionExtensions.cs +++ b/src/FluentAssertions.Analyzers/Utilities/OperartionExtensions.cs @@ -51,6 +51,16 @@ public static bool IsSameArgumentReference(this IArgumentOperation argument1, IA && argument2.TryGetFirstDescendent(out var argument2Reference) && argument1Reference.Parameter.Equals(argument2Reference.Parameter, SymbolEqualityComparer.Default); } + public static bool IsSamePropertyReference(this IPropertyReferenceOperation property1, IPropertyReferenceOperation property2) + { + return (property1.Instance is ILocalReferenceOperation local1 + && property2.Instance is ILocalReferenceOperation local2 + && local1.Local.Equals(local2.Local, SymbolEqualityComparer.Default)) + || + (property1.Instance is IParameterReferenceOperation parameter1 + && property2.Instance is IParameterReferenceOperation parameter2 + && parameter1.Parameter.Equals(parameter2.Parameter, SymbolEqualityComparer.Default)); + } public static bool IsLiteralValue(this IArgumentOperation argument, T value) => UnwrapConversion(argument.Value) is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value.Equals(value);