Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Rule: ExcludeFromCodeCoverage attributes should include a justification #6593

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a31056a
Crreate analyzers.
Dec 24, 2022
052c78b
Support null string.empty and whitsepace, ignore older versions of .NET.
Mar 9, 2023
fa7c65d
390+
Mar 9, 2023
830ccb5
Update analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCovera…
Corniel Mar 9, 2023
9dfe3e1
Update analyzers/tests/SonarAnalyzer.UnitTest/Rules/ExcludeFromCodeCo…
Corniel Mar 9, 2023
5f215a6
Update analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCo…
Corniel Mar 9, 2023
530a0ae
Get Justification content.
Mar 9, 2023
22535ed
Oops.
Mar 9, 2023
77c9a98
Update analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCovera…
Corniel Mar 9, 2023
5b7e905
Add test case to indicate type alias support.
Mar 9, 2023
3fe1bd4
Merge branch 'corniel/justify-exclude-from-code-coverage' of https://…
Mar 9, 2023
b537f13
Added comment.
Mar 9, 2023
93469be
Update analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCovera…
Corniel Mar 9, 2023
f285304
Move comment,
Mar 9, 2023
5098ff3
Merge branch 'corniel/justify-exclude-from-code-coverage' of https://…
Mar 9, 2023
5a5e61a
Indentation fix.
Mar 10, 2023
c46bd1c
Update analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCovera…
Corniel Mar 13, 2023
7d7bff5
Update analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCovera…
Corniel Mar 13, 2023
4e43f03
Run rspec script.
martin-strecker-sonarsource Mar 13, 2023
916c251
Whitespace
martin-strecker-sonarsource Mar 13, 2023
2342fd4
Update RSpec
martin-strecker-sonarsource Mar 14, 2023
bf74d6a
Fix file header
martin-strecker-sonarsource Mar 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ languages in [SonarQube](http://www.sonarqube.org/), [SonarCloud](https://sonarc

## Features

* [380+ C# rules](https://rules.sonarsource.com/csharp) and [170+ VB.​NET rules](https://rules.sonarsource.com/vbnet)
* [390+ C# rules](https://rules.sonarsource.com/csharp) and [170+ VB.​NET rules](https://rules.sonarsource.com/vbnet)
* Metrics (cognitive complexity, duplications, number of lines etc.)
* Import of [test coverage reports](https://community.sonarsource.com/t/9871) from Visual Studio Code Coverage, dotCover, OpenCover, Coverlet, Altcover.
* Import of third party Roslyn Analyzers results
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 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 ExcludeFromCodeCoverageAttributesNeedJustification : ExcludeFromCodeCoverageAttributesNeedJustificationBase<SyntaxKind>
{
protected override ILanguageFacade<SyntaxKind> Language => CSharpFacade.Instance;

protected override SyntaxNode GetJustificationExpression(SyntaxNode node) =>
node is AttributeSyntax { ArgumentList.Arguments: { Count: 1 } arguments }
&& arguments[0] is { NameEquals.Name.Identifier.ValueText: JustificationPropertyName, Expression: { } expression }
? expression
: null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 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;

public abstract class ExcludeFromCodeCoverageAttributesNeedJustificationBase<TSyntaxKind> : SonarDiagnosticAnalyzer<TSyntaxKind>
where TSyntaxKind : struct
{
private const string DiagnosticId = "S6513";
protected const string JustificationPropertyName = "Justification";

protected override string MessageFormat => "Add a justification.";

protected abstract SyntaxNode GetJustificationExpression(SyntaxNode node);

protected ExcludeFromCodeCoverageAttributesNeedJustificationBase() : base(DiagnosticId) { }

protected sealed override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(
Language.GeneratedCodeRecognizer,
c =>
{
if (NoJustification(c.Node, c.SemanticModel)
&& c.SemanticModel.GetSymbolInfo(c.Node).Symbol is IMethodSymbol attribute
&& attribute.IsInType(KnownType.System_Diagnostics_CodeAnalysis_ExcludeFromCodeCoverageAttribute)
&& HasJustificationProperty(attribute.ContainingType))
martin-strecker-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
{
c.ReportIssue(Diagnostic.Create(Rule, c.Node.GetLocation()));
}
},
Language.SyntaxKind.Attribute);

/// <summary>"Justification" was added in .Net 5 whileExcludeFromCodeCoverage in netstandard2.0.</summary>
martin-strecker-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
private bool NoJustification(SyntaxNode node, SemanticModel model) =>
martin-strecker-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
GetJustificationExpression(node) is not { } justification
|| string.IsNullOrWhiteSpace(Language.FindConstantValue(model, justification) as string);

private static bool HasJustificationProperty(INamedTypeSymbol symbol) =>
Corniel marked this conversation as resolved.
Show resolved Hide resolved
symbol.MemberNames.Contains(JustificationPropertyName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 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.VisualBasic;

[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
public sealed class ExcludeFromCodeCoverageAttributesNeedJustification : ExcludeFromCodeCoverageAttributesNeedJustificationBase<SyntaxKind>
{
protected override ILanguageFacade<SyntaxKind> Language => VisualBasicFacade.Instance;

protected override SyntaxNode GetJustificationExpression(SyntaxNode node) =>
node is AttributeSyntax { ArgumentList.Arguments: { Count: 1 } arguments }
&& arguments[0] is SimpleArgumentSyntax { Expression: { } } argument
&& Language.NameComparer.Equals(argument.GetName(), JustificationPropertyName)
? argument.Expression
: null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2022 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 CS = SonarAnalyzer.Rules.CSharp;
using VB = SonarAnalyzer.Rules.VisualBasic;

namespace SonarAnalyzer.UnitTest.Rules;

[TestClass]
public class ExcludeFromCodeCoverageAttributesNeedJustificationTest
{
private readonly VerifierBuilder builderCS = new VerifierBuilder<CS.ExcludeFromCodeCoverageAttributesNeedJustification>();
private readonly VerifierBuilder builderVB = new VerifierBuilder<VB.ExcludeFromCodeCoverageAttributesNeedJustification>();

#if NET
[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_OnAssembly_CS() =>
builderCS.AddSnippet("[assembly:System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // Noncompliant").Verify();

[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_OnAssembly_VB() =>
builderVB.AddSnippet("<Assembly:System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage> ' Noncompliant").Verify();

[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_CS() =>
builderCS.AddPaths("ExcludeFromCodeCoverageAttributesNeedJustification.cs").Verify();

[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_CSharp9() =>
builderCS.AddPaths("ExcludeFromCodeCoverageAttributesNeedJustification.CSharp9.cs").WithTopLevelStatements().Verify();

[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_CSharp10() =>
builderCS.AddPaths("ExcludeFromCodeCoverageAttributesNeedJustification.CSharp10.cs").WithOptions(ParseOptionsHelper.FromCSharp10).Verify();

[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_VB() =>
builderVB.AddPaths("ExcludeFromCodeCoverageAttributesNeedJustification.vb").Verify();
#endif

#if netframework
[TestMethod]
public void ExcludeFromCodeCoverageAttributesNeedJustification_IgnoredForNet48() =>
builderCS.AddPaths("ExcludeFromCodeCoverageAttributesNeedJustification.net48.cs").Verify();
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

class NonCompliant
{
public void Method(IEnumerable<int> collection)
{
[ExcludeFromCodeCoverage] int Get() => 1; // Noncompliant

_ = collection.Select([ExcludeFromCodeCoverage] (x) => x + 1); // Noncompliant

Action a = [ExcludeFromCodeCoverage] () => { }; // Noncompliant

Action x = true
? ([ExcludeFromCodeCoverage] () => { }) // Noncompliant
: [ExcludeFromCodeCoverage] () => { }; // Noncompliant

Call([ExcludeFromCodeCoverage(Justification = "justification")] (x) => { });
}

private void Call(Action<int> action) => action(1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using System;

[ExcludeFromCodeCoverage] // Noncompliant
void LocalMethod() { }

[ExcludeFromCodeCoverage] // Noncompliant
static void StaticLocalMethod() { }

[ExcludeFromCodeCoverage] // Noncompliant {{Add a justification.}}
public record Record
{
public void Method()
{
[ExcludeFromCodeCoverage] // Noncompliant
static void LocalMethod() { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Alias = System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute;

martin-strecker-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
[ExcludeFromCodeCoverage] // Noncompliant ^2#23 {{Add a justification.}}
class Noncompliant
{
[ExcludeFromCodeCoverage()] // Noncompliant
void WithBrackets() { }

[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // Noncompliant
void FullyDeclaredNamespace() { }

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // Noncompliant
void GloballyDeclaredNamespace() { }

[ExcludeFromCodeCoverage(Justification = null)] // Noncompliant
void WithNull() { }

[ExcludeFromCodeCoverage(Justification = "")] // Noncompliant
void WithEmptyString() { }

[ExcludeFromCodeCoverage(Justification = " ")] // Noncompliant
void WithWhiteSpace() { }

[Alias(Justification = "")] // Noncompliant
void WithAlias() { }

[ExcludeFromCodeCoverage] // Noncompliant
[CLSCompliant(false)]
uint Multiple() { return 0; }

[ExcludeFromCodeCoverage, CLSCompliant(false)]
// ^^^^^^^^^^^^^^^^^^^^^^^
uint Combined() { return 0; }

[ExcludeFromCodeCoverage] // Noncompliant
Noncompliant() { }

[ExcludeFromCodeCoverage] // Noncompliant
void Method() { }

[ExcludeFromCodeCoverage] // Noncompliant
event EventHandler Event;
}

interface IInterface
{
[ExcludeFromCodeCoverage] // Noncompliant
void Method();
}

[ExcludeFromCodeCoverage] // Noncompliant
struct ProgramStruct
{
[ExcludeFromCodeCoverage] // Noncompliant
void Method() { }
}
martin-strecker-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

[ExcludeFromCodeCoverage(Justification = "justification")]
class Compliant
{
[ExcludeFromCodeCoverage(Justification = "justification")]
Compliant() { }

[ExcludeFromCodeCoverage(Justification = "justification")]
void Method() { }

[ExcludeFromCodeCoverage(Justification = "justification")]
string Property { get; set; }

[ExcludeFromCodeCoverage(Justification = "justification")]
event EventHandler Event;
}

interface IComplaintInterface
{
[ExcludeFromCodeCoverage(Justification = "justification")]
void Method();
}

[ExcludeFromCodeCoverage(Justification = "justification")]
struct ComplaintStruct
{
[ExcludeFromCodeCoverage(Justification = "justification")]
void Method() { }
}

class NotApplicable
{
[CLSCompliant(false)]
enum Enum { foo, bar }

NotApplicable() { }

void Method() { }

int Property { get; set; }

int Field;

event EventHandler Event;

delegate void Delegate();

[OtherNamespace.ExcludeFromCodeCoverage]
void SameName() { }
}

namespace OtherNamespace
{
public class ExcludeFromCodeCoverageAttribute : Attribute { }
}
Loading