diff --git a/analyzers/rspec/cs/S2970_c#.html b/analyzers/rspec/cs/S2970_c#.html new file mode 100644 index 00000000000..7a5d5177ce6 --- /dev/null +++ b/analyzers/rspec/cs/S2970_c#.html @@ -0,0 +1,29 @@ +

It is very easy to write incomplete assertions when using some test frameworks. This rule enforces complete assertions in the following cases:

+ +

In such cases, what is intended to be a test doesn’t actually verify anything.

+

Noncompliant Code Example

+
+string actual = "Hello World!";
+// Fluent Assertions
+actual.Should();     // Noncompliant
+// NFluent
+Check.That(actual);  // Noncompliant
+// NSubstitute
+command.Received();  // Noncompliant
+
+

Compliant Solution

+
+string actual = "Hello World!";
+// Fluent Assertions
+actual.Should().Contain("Hello");
+// NFluent
+Check.That(actual).Contains("Hello");
+// NSubstitute
+command.Received().Execute();
+
+ diff --git a/analyzers/rspec/cs/S2970_c#.json b/analyzers/rspec/cs/S2970_c#.json new file mode 100644 index 00000000000..9cd57fbfeb4 --- /dev/null +++ b/analyzers/rspec/cs/S2970_c#.json @@ -0,0 +1,17 @@ +{ + "title": "Assertions should be complete", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "tests" + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-2970", + "sqKey": "S2970", + "scope": "Tests", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json index 2b7c15c5eb5..76e9af503d7 100644 --- a/analyzers/rspec/cs/Sonar_way_profile.json +++ b/analyzers/rspec/cs/Sonar_way_profile.json @@ -119,6 +119,7 @@ "S2933", "S2934", "S2953", + "S2970", "S2971", "S2995", "S2996", diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSyntaxHelper.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSyntaxHelper.cs index 8b30ece4ad5..4dd747bae43 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSyntaxHelper.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpSyntaxHelper.cs @@ -280,6 +280,11 @@ node switch public static bool NameIs(this SyntaxNode node, string name) => node.GetName().Equals(name, StringComparison.InvariantCulture); + public static bool NameIs(this SyntaxNode node, string name, params string[] orNames) => + node.GetName() is { } nodeName + && (nodeName.Equals(name, StringComparison.InvariantCulture) + || orNames.Any(x => nodeName.Equals(x, StringComparison.InvariantCulture))); + public static bool HasConstantValue(this ExpressionSyntax expression, SemanticModel semanticModel) => expression.RemoveParentheses().IsAnyKind(LiteralSyntaxKinds) || expression.FindConstantValue(semanticModel) != null; diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/AssertionsShouldBeComplete.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/AssertionsShouldBeComplete.cs new file mode 100644 index 00000000000..df4bcc22984 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/AssertionsShouldBeComplete.cs @@ -0,0 +1,131 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AssertionsShouldBeComplete : SonarDiagnosticAnalyzer +{ + private const string DiagnosticId = "S2970"; + private const string MessageFormat = "Complete the assertion"; + + private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterCompilationStartAction(start => + { + if (start.Compilation.References(KnownAssembly.FluentAssertions)) + { + start.RegisterNodeAction(c => + CheckInvocation(c, invocation => + invocation.NameIs("Should") + && c.SemanticModel.GetSymbolInfo(invocation).AllSymbols().Any(x => + x is IMethodSymbol + { + IsExtensionMethod: true, + ReturnsVoid: false, + ContainingType: { } container, + ReturnType: { } returnType, + } + && (container.Is(KnownType.FluentAssertions_AssertionExtensions) + // ⬆️ Built in assertions. ⬇️ Custom assertions (the majority at least). + || returnType.DerivesFrom(KnownType.FluentAssertions_Primitives_ReferenceTypeAssertions)))), + SyntaxKind.InvocationExpression); + } + if (start.Compilation.References(KnownAssembly.NFluent)) + { + start.RegisterNodeAction(c => + CheckInvocation(c, invocation => + invocation.NameIs("That", "ThatEnum", "ThatCode", "ThatAsyncCode", "ThatDynamic") + && c.SemanticModel.GetSymbolInfo(invocation) is + { + Symbol: IMethodSymbol + { + IsStatic: true, + ReturnsVoid: false, + ContainingType: { IsStatic: true } container + } + } + && container.Is(KnownType.NFluent_Check)), + SyntaxKind.InvocationExpression); + } + if (start.Compilation.References(KnownAssembly.NSubstitute)) + { + start.RegisterNodeAction(c => + CheckInvocation(c, invocation => + invocation.NameIs("Received", "DidNotReceive", "ReceivedWithAnyArgs", "DidNotReceiveWithAnyArgs", "ReceivedCalls") + && c.SemanticModel.GetSymbolInfo(invocation) is + { + Symbol: IMethodSymbol + { + IsExtensionMethod: true, + ReturnsVoid: false, + ContainingType: { } container, + } + } + && container.Is(KnownType.NSubstitute_SubstituteExtensions)), + SyntaxKind.InvocationExpression); + } + }); + + private static void CheckInvocation(SonarSyntaxNodeReportingContext c, Func isAssertionMethod) + { + if (c.Node is InvocationExpressionSyntax invocation + && isAssertionMethod(invocation) + && !HasContinuation(invocation)) + { + c.ReportIssue(Diagnostic.Create(Rule, invocation.GetIdentifier()?.GetLocation())); + } + } + + private static bool HasContinuation(InvocationExpressionSyntax invocation) + { + var closeParen = invocation.ArgumentList.CloseParenToken; + if (!closeParen.IsKind(SyntaxKind.CloseParenToken) || closeParen.IsMissing || !invocation.GetLastToken().Equals(closeParen)) + { + // Any invocation should end with ")". We are in unknown territory here. + return true; + } + if (closeParen.GetNextToken() is var nextToken + && !nextToken.IsKind(SyntaxKind.SemicolonToken)) + { + // There is something right to the invocation that is not a semicolon. + return true; + } + // We are in some kind of statement context "??? Should();" + // The result might be stored in a variable or returned from the method/property + return nextToken.Parent switch + { + MethodDeclarationSyntax { ReturnType: { } returnType } => !IsVoid(returnType), + { } parent when LocalFunctionStatementSyntaxWrapper.IsInstance(parent) => !IsVoid(((LocalFunctionStatementSyntaxWrapper)parent).ReturnType), + PropertyDeclarationSyntax => true, + AccessorDeclarationSyntax { Keyword.RawKind: (int)SyntaxKind.GetKeyword } => true, + ReturnStatementSyntax => true, + LocalDeclarationStatementSyntax => true, + ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax } => true, + _ => false, + }; + } + + private static bool IsVoid(TypeSyntax type) => + type is PredefinedTypeSyntax { Keyword.RawKind: (int)SyntaxKind.VoidKeyword }; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Extensions/SymbolInfoExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Extensions/SymbolInfoExtensions.cs new file mode 100644 index 00000000000..f667d11e8ec --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Extensions/SymbolInfoExtensions.cs @@ -0,0 +1,33 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace SonarAnalyzer.Extensions +{ + public static class SymbolInfoExtensions + { + /// + /// Returns the or if no symbol could be found the . + /// + public static IEnumerable AllSymbols(this SymbolInfo symbolInfo) => + symbolInfo.Symbol == null + ? symbolInfo.CandidateSymbols + : new[] { symbolInfo.Symbol }; + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs index d7f085bcefd..79b689de1c3 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs @@ -30,6 +30,19 @@ public sealed partial class KnownAssembly And(NameIs("xunit.assert").Or(NameIs("xunit").And(VersionLowerThen("2.0"))), PublicKeyTokenIs("8d05b1bb7a6fdb6c"))); + /// + /// Any MSTest framework either referenced via + /// nuget.org/MicrosoftVisualStudioQualityToolsUnitTestFramework (MSTest V1) + /// or nuget.org/MSTest.TestFramework (MSTest V2). + /// + public static KnownAssembly MSTest { get; } = + new(And(NameIs("Microsoft.VisualStudio.QualityTools.UnitTestFramework").Or(NameIs("Microsoft.VisualStudio.TestPlatform.TestFramework")), + PublicKeyTokenIs("b03f5f7f11d50a3a"))); + + public static KnownAssembly FluentAssertions { get; } = new(NameIs("FluentAssertions").And(PublicKeyTokenIs("33f2691a05b67b6a"))); + public static KnownAssembly NFluent { get; } = new(NameIs("NFluent").And(OptionalPublicKeyTokenIs("18828b37b84b1437"))); + public static KnownAssembly NSubstitute { get; } = new(NameIs("NSubstitute").And(PublicKeyTokenIs("92dd2e9066daa5ca"))); + internal KnownAssembly(Func predicate, params Func[] or) : this(predicate is null || or.Any(x => x is null) ? throw new ArgumentNullException(nameof(predicate), "All predicates must be non-null.") diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 7b0c7b39c21..9eaa2c8dccb 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -37,7 +37,9 @@ public sealed partial class KnownType public static readonly KnownType Azure_ResourceManager_ArmClient = new("Azure.ResourceManager.ArmClient"); public static readonly KnownType Dapper_SqlMapper = new("Dapper.SqlMapper"); public static readonly KnownType Dapper_CommandDefinition = new("Dapper.CommandDefinition"); + public static readonly KnownType FluentAssertions_AssertionExtensions = new("FluentAssertions.AssertionExtensions"); public static readonly KnownType FluentAssertions_Execution_AssertionScope = new("FluentAssertions.Execution.AssertionScope"); + public static readonly KnownType FluentAssertions_Primitives_ReferenceTypeAssertions = new("FluentAssertions.Primitives.ReferenceTypeAssertions", "TSubject", "TAssertions"); public static readonly KnownType JWT_Builder_JwtBuilder = new("JWT.Builder.JwtBuilder"); public static readonly KnownType JWT_IJwtDecoder = new("JWT.IJwtDecoder"); public static readonly KnownType JWT_JwtDecoderExtensions = new("JWT.JwtDecoderExtensions"); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/KnownAssemblyTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/KnownAssemblyTest.cs index 9170933ef03..c128ff60764 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/KnownAssemblyTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/KnownAssemblyTest.cs @@ -275,6 +275,84 @@ public void XUnitAssert_NoReference() compilation.References(XUnit_Assert).Should().BeFalse(); } + [TestMethod] + public void MSTest_V1() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.MSTestTestFrameworkV1.ToArray()).Model.Compilation; + compilation.References(MSTest).Should().BeTrue(); + } + + [TestMethod] + public void MSTest_V2() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.MSTestTestFramework("3.0.2").ToArray()).Model.Compilation; + compilation.References(MSTest).Should().BeTrue(); + } + + [TestMethod] + public void MSTest_MicrosoftVisualStudioQualityToolsUnitTestFramework() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.MicrosoftVisualStudioQualityToolsUnitTestFramework.ToArray()).Model.Compilation; + compilation.References(MSTest).Should().BeTrue(); + } + + [TestMethod] + public void MSTest_NoReference() + { + var compilation = TestHelper.CompileCS("// Empty file").Model.Compilation; + compilation.References(MSTest).Should().BeFalse(); + } + + [TestMethod] + public void FluentAssertions_6_10() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.FluentAssertions("6.10.0").ToArray()).Model.Compilation; + compilation.References(KnownAssembly.FluentAssertions).Should().BeTrue(); + } + + [TestMethod] + public void FluentAssertions_NoReference() + { + var compilation = TestHelper.CompileCS("// Empty file").Model.Compilation; + compilation.References(KnownAssembly.FluentAssertions).Should().BeFalse(); + } + + [TestMethod] + public void NFluent_2_8() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.NFluent("2.8.0").ToArray()).Model.Compilation; + compilation.References(NFluent).Should().BeTrue(); + } + + [TestMethod] + public void NFluent_1_0() + { + // 1.0.0 has no publicKeyToken + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.NFluent("1.0.0").ToArray()).Model.Compilation; + compilation.References(NFluent).Should().BeTrue(); + } + + [TestMethod] + public void NFluent_NoReference() + { + var compilation = TestHelper.CompileCS("// Empty file").Model.Compilation; + compilation.References(NFluent).Should().BeFalse(); + } + + [TestMethod] + public void NSubstitute_5_0() + { + var compilation = TestHelper.CompileCS("// Empty file", NuGetMetadataReference.NSubstitute("5.0.0").ToArray()).Model.Compilation; + compilation.References(NSubstitute).Should().BeTrue(); + } + + [TestMethod] + public void NSubstitute_NoReference() + { + var compilation = TestHelper.CompileCS("// Empty file").Model.Compilation; + compilation.References(NSubstitute).Should().BeFalse(); + } + private static Mock CompilationWithReferenceTo(AssemblyIdentity identity) { var compilation = new Mock("compilationName", ImmutableArray.Empty, new Dictionary(), false, null, null); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SyntaxHelperTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SyntaxHelperTest.cs index 6bd0e120890..d61c17da915 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SyntaxHelperTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/SyntaxHelperTest.cs @@ -137,6 +137,25 @@ public void NameIs_CS() toString.NameIs(null).Should().BeFalse(); } + [DataTestMethod] + [DataRow(true, "Test")] + [DataRow(true, "Test", "Test")] + [DataRow(true, "Other", "Test")] + [DataRow(false)] + [DataRow(false, "TEST")] + public void NameIsOrNames_CS(bool expected, params string[] orNames) + { + var identifier = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.IdentifierName("Test"); + identifier.NameIs("other", orNames).Should().Be(expected); + } + + [TestMethod] + public void NameIsOrNamesNodeWithoutName_CS() + { + var returnStatement = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ReturnStatement(); + returnStatement.NameIs("A", "B", "C").Should().BeFalse(); + } + [TestMethod] public void NameIs_VB() { diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs index 4dc49032b0f..03ae2298a3f 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs @@ -2894,7 +2894,7 @@ internal static class RuleTypeMappingCS // ["S2967"], // ["S2968"], // ["S2969"], - // ["S2970"], + ["S2970"] = "CODE_SMELL", ["S2971"] = "CODE_SMELL", // ["S2972"], // ["S2973"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AssertionsShouldBeCompleteTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AssertionsShouldBeCompleteTest.cs new file mode 100644 index 00000000000..3230246c6a2 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AssertionsShouldBeCompleteTest.cs @@ -0,0 +1,109 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarAnalyzer.Rules.CSharp; +using SonarAnalyzer.UnitTest.MetadataReferences; + +namespace SonarAnalyzer.UnitTest.Rules; + +[TestClass] +public class AssertionsShouldBeCompleteTest +{ + private readonly VerifierBuilder fluentAssertions = new VerifierBuilder() + .AddReferences(NuGetMetadataReference.FluentAssertions("6.10.0")) + .AddReferences(MetadataReferenceFacade.SystemXml) + .AddReferences(MetadataReferenceFacade.SystemXmlLinq) + .AddReferences(MetadataReferenceFacade.SystemNetHttp) + .AddReferences(MetadataReferenceFacade.SystemData); + + private readonly VerifierBuilder nfluent = new VerifierBuilder() + .AddReferences(NuGetMetadataReference.NFluent("2.8.0")); + + private readonly VerifierBuilder nsubstitute = new VerifierBuilder() + .AddReferences(NuGetMetadataReference.NSubstitute("5.0.0")); + + private readonly VerifierBuilder allFrameworks; + + public AssertionsShouldBeCompleteTest() + { + allFrameworks = new VerifierBuilder() + .AddReferences(fluentAssertions.References) + .AddReferences(nfluent.References) + .AddReferences(nsubstitute.References); + } + + [TestMethod] + public void AssertionsShouldBeComplete_FluentAssertions_CSharp7() => + fluentAssertions + .WithOptions(ParseOptionsHelper.OnlyCSharp7) + .AddPaths("AssertionsShouldBeComplete.FluentAssertions.CSharp7.cs") + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_FluentAssertions_MissingParen() => + fluentAssertions + .AddSnippet(""" + using FluentAssertions; + public class Test + { + public void MissingParen() + { + var s = "Test"; + s.Should(; // Error + } + } + """) + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_FluentAssertions_CSharp8() => + fluentAssertions + .WithOptions(ParseOptionsHelper.FromCSharp8) + .AddPaths("AssertionsShouldBeComplete.FluentAssertions.CSharp8.cs") + .WithConcurrentAnalysis(false) + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_NFluent_CSharp() => + nfluent + .AddTestReference() + .AddPaths("AssertionsShouldBeComplete.NFluent.cs") + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_NFluent_CSharp11() => + nfluent + .WithOptions(ParseOptionsHelper.FromCSharp11) + .AddTestReference() + .AddPaths("AssertionsShouldBeComplete.NFluent.CSharp11.cs") + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_NSubstitute_CS() => + nsubstitute + .AddPaths("AssertionsShouldBeComplete.NSubstitute.cs") + .Verify(); + + [TestMethod] + public void AssertionsShouldBeComplete_AllFrameworks_CS() => + allFrameworks + .AddPaths("AssertionsShouldBeComplete.AllFrameworks.cs") + .Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.AllFrameworks.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.AllFrameworks.cs new file mode 100644 index 00000000000..a213fa5c305 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.AllFrameworks.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using NFluent; +using NSubstitute; +using System; + +namespace AllFrameworksTests +{ + internal class Tests + { + public void FluentAssertions(string s) + { + s.Should(); // Noncompliant + } + + public void NFluent() + { + Check.That(0); // Noncompliant + } + + public void NSubstitute(IComparable comparable) + { + comparable.Received(); // Noncompliant + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp7.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp7.cs new file mode 100644 index 00000000000..2d1c422f4a9 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp7.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using FluentAssertions.Primitives; + +public class Program +{ + public void StringAssertions() + { + var s = "Test"; + s.Should(); // Noncompliant {{Complete the assertion}} + //^^^^^^ + s[0].Should(); // Error [CS0121] ambiguous calls + // Noncompliant@-1 + + s.Should().Be("Test"); // Compliant + } + + public void CollectionAssertions() + { + var collection = new[] { "Test", "Test" }; + collection.Should(); // Error [CS0121] ambiguous calls + // Noncompliant@-1 + collection.Should(); // Noncompliant + + collection.Should().Equal("Test", "Test"); // Compliant + } + + public void DictionaryAssertions() + { + var dict = new Dictionary(); + dict["A"].Should(); // Noncompliant + } + + public StringAssertions ReturnedByReturn() + { + var s = "Test"; + return s.Should(); // Compliant + } + + public StringAssertions ReturnedByArrow(string s) => + s.Should(); // Compliant + + public object ReturnedByArrowWithConversion(string s) => + (object)s.Should(); // Compliant + + public void CalledByArrow(string s) => + s.Should(); // Noncompliant + + public void Assigned() + { + var s = "Test"; + var assertion = s.Should(); // Compliant + assertion = s.Should(); // Compliant + } + + public void PassedAsArgument() + { + var s = "Test"; + ValidateString(s.Should()); // Compliant + } + private void ValidateString(StringAssertions assertion) { } + + public void UnreducedCall() + { + var s = "Test"; + FluentAssertions.AssertionExtensions.Should(s); // Noncompliant + } + + public void ReturnedInLambda() + { + Func a = () => "Test".Should(); + } + + public void CustomAssertions() + { + var custom = new Custom(); + custom.Should(); // Noncompliant The custom assertion derives from ReferenceTypeAssertions + custom.Should().BeCustomAsserted(); // Compliant + } + + public void CustomStructAssertions() + { + var custom = new CustomStruct(); + custom.Should(); // Compliant Potential FN. CustomStructAssertion does not derive from ReferenceTypeAssertions and is not considered a custom validation. + custom.Should().BeCustomAsserted(); // Compliant + } +} + +public static class CustomAssertionExtension +{ + public static CustomAssertion Should(this Custom instance) + => new CustomAssertion(instance); +} + +public class CustomAssertion : ReferenceTypeAssertions +{ + public CustomAssertion(Custom instance) + : base(instance) + { + } + + protected override string Identifier => "custom"; + + public void BeCustomAsserted() + { + // Not implemented + } +} + +public class Custom { } + +public static class CustomStructAssertionExtension +{ + public static CustomStructAssertion Should(this CustomStruct instance) + => new CustomStructAssertion(instance); +} + +public class CustomStructAssertion // Does not derive from ReferenceTypeAssertions +{ + public CustomStructAssertion(CustomStruct instance) { } + + public void BeCustomAsserted() + { + // Not implemented + } +} + +public struct CustomStruct { } + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp8.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp8.cs new file mode 100644 index 00000000000..fda6f1dafab --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.FluentAssertions.CSharp8.cs @@ -0,0 +1,70 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using FluentAssertions; +using FluentAssertions.Primitives; + +public class Program +{ + public void StringAssertions() + { + var s = "Test"; + s.Should(); // Noncompliant + s?.Should(); // Noncompliant + s[0].Should(); // Noncompliant (no "ambiguous calls" compiler error as in C# 7) + s?[0].Should(); // Noncompliant + s.Should().Be("Test"); // Compliant + s.Should()?.Be("Test"); // Compliant + s.Should()!?.Be("Test"); // Compliant + } + + public void CollectionAssertions() + { + var collection = new[] { "Test", "Test" }; + collection.Should(); // Noncompliant (no "ambiguous calls" compiler error as in C# 7) + } + + public void DictAssertions() + { + var dict = new Dictionary(); + dict["A"]?.Should(); // Noncompliant + // ^^^^^^ + dict?["A"]?.Should(); // Noncompliant + // ^^^^^^ + dict?["A"]!.Should(); // Noncompliant + // ^^^^^^ + } + + public void LocalFunction() + { + var s = "Test"; + + StringAssertions ExpressionBodyLocalFunction() => + s.Should(); + + void VoidReturningExpressionBodyLocalFunction() => + s.Should(); // Noncompliant + + StringAssertions ReturnLocalFunction() + { + return s.Should(); + } + } + + public StringAssertions PropertyArrow => "Test".Should(); + + public StringAssertions PropertyGetterArrow + { + get => "Test".Should(); + set => "Test".Should(); // Noncompliant + } + + public StringAssertions PropertyGetterReturn + { + get + { + return "Test".Should(); + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.CSharp11.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.CSharp11.cs new file mode 100644 index 00000000000..6ffb58c0166 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.CSharp11.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using NFluent; +using static NFluent.Check; + +public class Program +{ + public void CheckThat() + { + That(0); // Noncompliant {{Complete the assertion}} + // ^^^^ + That(); // Noncompliant + + That(1).IsEqualTo(1); + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.cs new file mode 100644 index 00000000000..2d2a904e6d4 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NFluent.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using NFluent; + +public class Program +{ + public void CheckThat() + { + Check.That(0); // Noncompliant {{Complete the assertion}} + // ^^^^ + Check.That(); // Noncompliant + + Check.That(1).IsEqualTo(1); + } + + public void CheckThatEnum() + { + Check.ThatEnum(AttributeTargets.All); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^ + Check.ThatEnum(AttributeTargets.All); // Noncompliant + + Check.ThatEnum(AttributeTargets.All).IsEqualTo(AttributeTargets.All); + } + + public void CheckThatCode() + { + Check.ThatCode(() => { }); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^ + Check.ThatCode(() => 1); // Noncompliant + Check.ThatCode(CheckThatCode); // Noncompliant + + Check.ThatCode(() => { }).DoesNotThrow(); + } + + public async Task CheckThatAsyncCode() + { + Check.ThatAsyncCode(async () => await Task.CompletedTask); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^^^ + Check.ThatAsyncCode(async () => await Task.FromResult(1)); // Noncompliant + Check.ThatAsyncCode(CheckThatAsyncCode); // Noncompliant + + Check.ThatAsyncCode(async () => await Task.CompletedTask).DoesNotThrow(); + } + + public async Task CheckThatDynamic(dynamic expando) + { + Check.ThatDynamic(1); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^ + Check.ThatDynamic(expando); // Noncompliant + + Check.ThatDynamic(1).IsNotNull(); + } + + public ICheck CheckReturnedByReturn() + { + return Check.That(1); + } + + public ICheck CheckReturnedByExpressionBody() => + Check.That(1); + + public void AnonymousInvocation(Func a) => + a()(); +} + +namespace OtherCheck +{ + public static class Check + { + public static void That(int i) { } + } + + public class Test + { + public void CheckThat() + { + Check.That(1); // Compliant. Not NFluent + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NSubstitute.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NSubstitute.cs new file mode 100644 index 00000000000..8952f033795 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AssertionsShouldBeComplete.NSubstitute.cs @@ -0,0 +1,81 @@ +using NSubstitute; + +public interface ICommand +{ + void Execute(); +} + +namespace NSubstituteTests +{ + internal class Tests + { + public void Received() + { + var command = Substitute.For(); + command.Received(); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^ + command.Received(requiredNumberOfCalls: 2); // Noncompliant + command.Received(); // Noncompliant + SubstituteExtensions.Received(command); // Noncompliant + + command.Received().Execute(); + } + + public void DidNotReceive() + { + var command = Substitute.For(); + command.DidNotReceive(); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^^^ + command.DidNotReceive(); // Noncompliant + + command.DidNotReceive().Execute(); + } + + public void ReceivedWithAnyArgs() + { + var command = Substitute.For(); + command.ReceivedWithAnyArgs(); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^^^^^^^^^ + command.ReceivedWithAnyArgs(requiredNumberOfCalls: 2); // Noncompliant + command.ReceivedWithAnyArgs(); // Noncompliant + + command.ReceivedWithAnyArgs().Execute(); + } + + public void DidNotReceiveWithAnyArgs() + { + var command = Substitute.For(); + command.DidNotReceiveWithAnyArgs(); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^^^^^^^^^^^^^^ + command.DidNotReceiveWithAnyArgs(); // Noncompliant + + command.DidNotReceiveWithAnyArgs().Execute(); + } + + public void ReceivedCalls() + { + var command = Substitute.For(); + command.ReceivedCalls(); // Noncompliant {{Complete the assertion}} + // ^^^^^^^^^^^^^ + command.ReceivedCalls(); // Noncompliant + + var calls = command.ReceivedCalls(); + } + } +} + +namespace OtherReceived +{ + public static class OtherExtensions + { + public static void Received(this T something) { } + } + + public class Test + { + public void Received(ICommand command) + { + command.Received(); // Compliant. Other Received call + } + } +}