diff --git a/README.md b/README.md
index 31ba9bacb4d..ebb662793cd 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/analyzers/rspec/cs/S6513_c#.html b/analyzers/rspec/cs/S6513_c#.html
new file mode 100644
index 00000000000..3d61921fdf4
--- /dev/null
+++ b/analyzers/rspec/cs/S6513_c#.html
@@ -0,0 +1,53 @@
+
The ExcludeFromCodeCoverageAttribute is
+used to exclude portions of code from code coverage
+reporting. It is a bad practice to retain code that is not covered by unit tests. In .Net 5, the Justification
property was added to
+the ExcludeFromCodeCoverageAttribute
as an opportunity to document the rationale for the exclusion. This rule raises an issue when no
+such justification is given.
+Noncompliant Code Example
+
+public struct Coordinates
+{
+ public int X { get; }
+ public int Y { get; }
+
+ [ExcludeFromCodeCoverage] // Noncompliant
+ public override bool Equals(object obj) => obj is Coordinates coordinates && X == coordinates.X && Y == coordinates.Y;
+
+ [ExcludeFromCodeCoverage] // Noncompliant
+ public override int GetHashCode()
+ {
+ var hashCode = 1861411795;
+ hashCode = hashCode * -1521134295 + X.GetHashCode();
+ hashCode = hashCode * -1521134295 + Y.GetHashCode();
+ return hashCode;
+ }
+}
+
+Compliant Solution
+
+public struct Coordinates
+{
+ public int X { get; }
+ public int Y { get; }
+
+ [ExcludeFromCodeCoverage(Justification = "Code generated by Visual Studio refactoring")] // Compliant
+ public override bool Equals(object obj) => obj is Coordinates coordinates && X == coordinates.X && Y == coordinates.Y;
+
+ [ExcludeFromCodeCoverage(Justification = "Code generated by Visual Studio refactoring")] // Compliant
+ public override int GetHashCode()
+ {
+ var hashCode = 1861411795;
+ hashCode = hashCode * -1521134295 + X.GetHashCode();
+ hashCode = hashCode * -1521134295 + Y.GetHashCode();
+ return hashCode;
+ }
+}
+
+See
+
+
diff --git a/analyzers/rspec/cs/S6513_c#.json b/analyzers/rspec/cs/S6513_c#.json
new file mode 100644
index 00000000000..ed7960cee55
--- /dev/null
+++ b/analyzers/rspec/cs/S6513_c#.json
@@ -0,0 +1,17 @@
+{
+ "title": "\"ExcludeFromCodeCoverage\" attributes should include a justification",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "bad-practice"
+ ],
+ "defaultSeverity": "Minor",
+ "ruleSpecification": "RSPEC-6513",
+ "sqKey": "S6513",
+ "scope": "Main",
+ "quickfix": "unknown"
+}
diff --git a/analyzers/rspec/vbnet/S6513_vb.net.html b/analyzers/rspec/vbnet/S6513_vb.net.html
new file mode 100644
index 00000000000..b17edc5ad00
--- /dev/null
+++ b/analyzers/rspec/vbnet/S6513_vb.net.html
@@ -0,0 +1,67 @@
+The ExcludeFromCodeCoverageAttribute is
+used to exclude portions of code from code coverage
+reporting. It is a bad practice to retain code that is not covered by unit tests. In .Net 5, the Justification
property was added to
+the ExcludeFromCodeCoverageAttribute
as an opportunity to document the rationale for the exclusion. This rule raises an issue when no
+such justification is given.
+Noncompliant Code Example
+
+Public Structure Coordinates
+
+ Public ReadOnly Property X As Integer
+ Public ReadOnly Property Y As Integer
+
+ <ExcludeFromCodeCoverage> ' Noncompliant
+ Public Overrides Function Equals(obj As Object) As Boolean
+ If Not (TypeOf obj Is Coordinates) Then
+ Return False
+ End If
+
+ Dim coordinates = DirectCast(obj, Coordinates)
+ Return X = coordinates.X AndAlso
+ Y = coordinates.Y
+ End Function
+
+ <ExcludeFromCodeCoverage> ' Noncompliant
+ Public Overrides Function GetHashCode() As Integer
+ Dim hashCode As Long = 1861411795
+ hashCode = (hashCode * -1521134295 + X.GetHashCode()).GetHashCode()
+ hashCode = (hashCode * -1521134295 + Y.GetHashCode()).GetHashCode()
+ Return hashCode
+ End Function
+End Structure
+
+Compliant Solution
+
+Public Structure Coordinates
+
+ Public ReadOnly Property X As Integer
+ Public ReadOnly Property Y As Integer
+
+ <ExcludeFromCodeCoverage(Justification:="Code generated by Visual Studio refactoring")> ' Compliant
+ Public Overrides Function Equals(obj As Object) As Boolean
+ If Not (TypeOf obj Is Coordinates) Then
+ Return False
+ End If
+
+ Dim coordinates = DirectCast(obj, Coordinates)
+ Return X = coordinates.X AndAlso
+ Y = coordinates.Y
+ End Function
+
+ <ExcludeFromCodeCoverage(Justification:="Code generated by Visual Studio refactoring")> ' Compliant
+ Public Overrides Function GetHashCode() As Integer
+ Dim hashCode As Long = 1861411795
+ hashCode = (hashCode * -1521134295 + X.GetHashCode()).GetHashCode()
+ hashCode = (hashCode * -1521134295 + Y.GetHashCode()).GetHashCode()
+ Return hashCode
+ End Function
+End Structure
+
+See
+
+
diff --git a/analyzers/rspec/vbnet/S6513_vb.net.json b/analyzers/rspec/vbnet/S6513_vb.net.json
new file mode 100644
index 00000000000..ed7960cee55
--- /dev/null
+++ b/analyzers/rspec/vbnet/S6513_vb.net.json
@@ -0,0 +1,17 @@
+{
+ "title": "\"ExcludeFromCodeCoverage\" attributes should include a justification",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "bad-practice"
+ ],
+ "defaultSeverity": "Minor",
+ "ruleSpecification": "RSPEC-6513",
+ "sqKey": "S6513",
+ "scope": "Main",
+ "quickfix": "unknown"
+}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.cs
new file mode 100644
index 00000000000..5db33bed676
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.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.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ExcludeFromCodeCoverageAttributesNeedJustification : ExcludeFromCodeCoverageAttributesNeedJustificationBase
+{
+ protected override ILanguageFacade 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;
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationBase.cs
new file mode 100644
index 00000000000..16c42322b93
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.Common/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationBase.cs
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+public abstract class ExcludeFromCodeCoverageAttributesNeedJustificationBase : SonarDiagnosticAnalyzer
+ 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))
+ {
+ c.ReportIssue(Diagnostic.Create(Rule, c.Node.GetLocation()));
+ }
+ },
+ Language.SyntaxKind.Attribute);
+
+ private bool NoJustification(SyntaxNode node, SemanticModel model) =>
+ GetJustificationExpression(node) is not { } justification
+ || string.IsNullOrWhiteSpace(Language.FindConstantValue(model, justification) as string);
+
+ /// "Justification" was added in .Net 5, while ExcludeFromCodeCoverage in netstandard2.0.
+ private static bool HasJustificationProperty(INamedTypeSymbol symbol) =>
+ symbol.MemberNames.Contains(JustificationPropertyName);
+}
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.cs
new file mode 100644
index 00000000000..093af77ebba
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ExcludeFromCodeCoverageAttributesNeedJustification.cs
@@ -0,0 +1,34 @@
+/*
+ * 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.VisualBasic;
+
+[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
+public sealed class ExcludeFromCodeCoverageAttributesNeedJustification : ExcludeFromCodeCoverageAttributesNeedJustificationBase
+{
+ protected override ILanguageFacade 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;
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
index a79296bd910..cec58c9beb0 100644
--- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
@@ -6437,7 +6437,7 @@ internal static class RuleTypeMappingCS
// ["S6510"],
// ["S6511"],
// ["S6512"],
- // ["S6513"],
+ ["S6513"] = "CODE_SMELL",
// ["S6514"],
// ["S6515"],
// ["S6516"],
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
index b6d6396c5bc..47d0f2d65af 100644
--- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
@@ -6437,7 +6437,7 @@ internal static class RuleTypeMappingVB
// ["S6510"],
// ["S6511"],
// ["S6512"],
- // ["S6513"],
+ ["S6513"] = "CODE_SMELL",
// ["S6514"],
// ["S6515"],
// ["S6516"],
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationTest.cs
new file mode 100644
index 00000000000..8d36703ea21
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ExcludeFromCodeCoverageAttributesNeedJustificationTest.cs
@@ -0,0 +1,68 @@
+/*
+ * 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();
+ private readonly VerifierBuilder builderVB = new VerifierBuilder();
+
+#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(" ' 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
+
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp10.cs
new file mode 100644
index 00000000000..acac372d875
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp10.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+class NonCompliant
+{
+ public void Method(IEnumerable 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 action) => action(1);
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp9.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp9.cs
new file mode 100644
index 00000000000..5c4f6589d2c
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.CSharp9.cs
@@ -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() { }
+ }
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.cs
new file mode 100644
index 00000000000..0b64c7a14fd
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Alias = System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute;
+
+[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() { }
+}
+
+[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 { }
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.net48.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.net48.cs
new file mode 100644
index 00000000000..e4c7bed3892
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.net48.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+[ExcludeFromCodeCoverage]
+class Ignores
+{
+ [ExcludeFromCodeCoverage(Justification = "not existing")] // Error[CS0246]
+ void JustifcationDoesNotExist() { }
+
+ [ExcludeFromCodeCoverage()] // Compliant: "Justification" property was added in .Net 5
+ void WithBrackets() { }
+
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ void FullyDeclaredNamespace() { }
+
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ void GloballyDeclaredNamespace() { }
+
+ [ExcludeFromCodeCoverage]
+ [CLSCompliant(false)]
+ uint Multiple() { return 0; }
+
+ [ExcludeFromCodeCoverage, CLSCompliant(false)]
+ uint Combined() { return 0; }
+
+ [ExcludeFromCodeCoverage]
+ Ignores() { }
+
+ [ExcludeFromCodeCoverage]
+ void Method() { }
+
+ [ExcludeFromCodeCoverage]
+ event EventHandler Event;
+}
+
+interface IInterface
+{
+ [ExcludeFromCodeCoverage]
+ void Method();
+}
+
+[ExcludeFromCodeCoverage]
+struct ProgramStruct
+{
+ [ExcludeFromCodeCoverage]
+ void Method() { }
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.vb
new file mode 100644
index 00000000000..6446f8eb3b1
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ExcludeFromCodeCoverageAttributesNeedJustification.vb
@@ -0,0 +1,123 @@
+' Commented line for concurrent namespace
+Imports System.Diagnostics.CodeAnalysis
+Imports [Alias] = System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute
+
+ ' Noncompliant^2#23 {{Add a justification.}}
+Class Noncompliant
+ Sub WithBrackets() ' Noncompliant^6#25
+ End Sub
+
+ ' Noncompliant
+ Sub FullyDeclaredNamespace()
+ End Sub
+
+ ' Noncompliant
+ Sub GloballyDeclaredNamespace()
+ End Sub
+
+ <[Alias](Justification:="")> ' Noncompliant
+ Sub WithAlias()
+ End Sub
+
+ ' Noncompliant
+ Sub WithNothing() { }
+ End Sub
+
+ ' Noncompliant
+ Sub WithEmptyString() { }
+ End Sub
+
+ ' Noncompliant
+ Sub WithWhiteSpace() { }
+ End Sub
+
+ ' Noncompliant
+
+ Function Multiple() As UInteger
+ Return 0
+ End Function
+
+ Function Combined() As UInteger ' Noncompliant
+ Return 0
+ End Function
+
+ ' Noncompliant
+ Sub New()
+ End Sub
+
+ ' Noncompliant
+ Sub Method()
+ End Sub
+
+ ' Noncompliant
+ Property [Property] As Integer
+
+ ' Noncompliant
+ Event [Event] As EventHandler
+
+End Class
+
+Interface IInterface
+ ' Noncompliant
+ Sub Method()
+End Interface
+
+ ' Noncompliant
+Structure ProgramStruct
+ ' Noncompliant
+ Sub Method()
+ End Sub
+End Structure
+
+
+Class Compliant
+
+
+ Sub New()
+ End Sub
+
+
+ Sub Method()
+ End Sub
+
+
+ Property [Property] As String
+
+
+ Event [Event] As EventHandler
+End Class
+
+Interface IComplaintInterface
+
+ Sub Method()
+End Interface
+
+
+Structure ComplaintStruct
+
+ Sub Method()
+ End Sub
+End Structure
+
+Class NotApplicable
+
+ Sub New()
+ End Sub
+
+ Sub Method()
+ End Sub
+
+ Property [Property] As Integer
+
+ Event [Event] As EventHandler
+
+
+ Sub SameName()
+ End Sub
+End Class
+
+Namespace NotSystem
+ Public Class ExcludeFromCodeCoverageAttribute
+ Inherits Attribute
+ End Class
+End Namespace