From fddcf25f041533f52b9503c64af28b4a208b4a7d Mon Sep 17 00:00:00 2001
From: mfogliatto <2962955+mfogliatto@users.noreply.github.com>
Date: Thu, 28 May 2026 08:06:21 +0000
Subject: [PATCH] feat: enable project-level exceptions in ReferenceCop ruleset
Add an Exceptions element to rules that allows specifying projects
that are exempt from a given rule. Each rule can now contain:
- Added ProjectException class and Exceptions list to Rule base class
- Added IsProjectExempt() method for checking exemptions
- Updated all three violation detectors to check exceptions
- Updated XSD schema with Exceptions element for all rule types
- Added unit tests for the new functionality
Fixes mfogliatto/ReferenceCop#29
---
ReferenceCopConfig.xsd | 33 ++++
.../ReferenceCopAnalyzer.cs | 2 +-
.../Detectors/ProjectExceptionTests.cs | 175 ++++++++++++++++++
.../Configuration/ReferenceCopConfig.cs | 39 ++++
.../AssemblyNameViolationDetector.cs | 20 +-
.../Detectors/ProjectPathViolationDetector.cs | 6 +
.../Detectors/ProjectTagViolationDetector.cs | 6 +
7 files changed, 277 insertions(+), 4 deletions(-)
create mode 100644 src/ReferenceCop.Tests/Detectors/ProjectExceptionTests.cs
diff --git a/ReferenceCopConfig.xsd b/ReferenceCopConfig.xsd
index 96d0bcf..1fd9425 100644
--- a/ReferenceCopConfig.xsd
+++ b/ReferenceCopConfig.xsd
@@ -15,6 +15,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -26,6 +37,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -37,6 +59,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
index 81fb862..aab8841 100644
--- a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
+++ b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
@@ -42,7 +42,7 @@ public override void Initialize(AnalysisContext context)
{
var configLoader = new XmlConfigurationLoader(compilationAnalysisContext);
this.config = configLoader.Load();
- this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), this.config);
+ this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), this.config, compilationAnalysisContext.Compilation.AssemblyName);
if (this.config.EnableDebugMessages && this.config.UseExperimentalDetectors)
{
diff --git a/src/ReferenceCop.Tests/Detectors/ProjectExceptionTests.cs b/src/ReferenceCop.Tests/Detectors/ProjectExceptionTests.cs
new file mode 100644
index 0000000..d71d6f1
--- /dev/null
+++ b/src/ReferenceCop.Tests/Detectors/ProjectExceptionTests.cs
@@ -0,0 +1,175 @@
+namespace ReferenceCop.Tests
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using FluentAssertions;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class ProjectExceptionTests
+ {
+ [TestMethod]
+ public void IsProjectExempt_WhenProjectInExceptions_ReturnsTrue()
+ {
+ // Arrange.
+ var rule = new ReferenceCopConfig.AssemblyName
+ {
+ Name = "TestRule",
+ Pattern = "System.Xml",
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "MyProject" },
+ },
+ };
+
+ // Act & Assert.
+ rule.IsProjectExempt("MyProject").Should().BeTrue();
+ }
+
+ [TestMethod]
+ public void IsProjectExempt_WhenProjectNotInExceptions_ReturnsFalse()
+ {
+ // Arrange.
+ var rule = new ReferenceCopConfig.AssemblyName
+ {
+ Name = "TestRule",
+ Pattern = "System.Xml",
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "OtherProject" },
+ },
+ };
+
+ // Act & Assert.
+ rule.IsProjectExempt("MyProject").Should().BeFalse();
+ }
+
+ [TestMethod]
+ public void IsProjectExempt_WhenNoExceptions_ReturnsFalse()
+ {
+ // Arrange.
+ var rule = new ReferenceCopConfig.AssemblyName
+ {
+ Name = "TestRule",
+ Pattern = "System.Xml",
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ };
+
+ // Act & Assert.
+ rule.IsProjectExempt("MyProject").Should().BeFalse();
+ }
+
+ [TestMethod]
+ public void IsProjectExempt_IsCaseInsensitive()
+ {
+ // Arrange.
+ var rule = new ReferenceCopConfig.AssemblyName
+ {
+ Name = "TestRule",
+ Pattern = "System.Xml",
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "MyProject" },
+ },
+ };
+
+ // Act & Assert.
+ rule.IsProjectExempt("myproject").Should().BeTrue();
+ rule.IsProjectExempt("MYPROJECT").Should().BeTrue();
+ }
+
+ [TestMethod]
+ public void GetViolationsFrom_WhenProjectIsExempt_DoesNotReportViolation()
+ {
+ // Arrange.
+ const string pattern = "System.Xml";
+ var config = new ReferenceCopConfig();
+ config.Rules.Add(new ReferenceCopConfig.AssemblyName
+ {
+ Name = "NoXml",
+ Pattern = pattern,
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "ExemptProject" },
+ },
+ });
+
+ var detector = new AssemblyNameViolationDetector(new ExactMatchComparer(), config, "ExemptProject");
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(pattern)),
+ };
+
+ // Act.
+ var violations = detector.GetViolationsFrom(references).ToList();
+
+ // Assert.
+ violations.Should().BeEmpty("because ExemptProject is in the rule's exceptions");
+ }
+
+ [TestMethod]
+ public void GetViolationsFrom_WhenProjectIsNotExempt_ReportsViolation()
+ {
+ // Arrange.
+ const string pattern = "System.Xml";
+ var config = new ReferenceCopConfig();
+ config.Rules.Add(new ReferenceCopConfig.AssemblyName
+ {
+ Name = "NoXml",
+ Pattern = pattern,
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "OtherProject" },
+ },
+ });
+
+ var detector = new AssemblyNameViolationDetector(new ExactMatchComparer(), config, "NonExemptProject");
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(pattern)),
+ };
+
+ // Act.
+ var violations = detector.GetViolationsFrom(references).ToList();
+
+ // Assert.
+ violations.Should().HaveCount(1);
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenProjectIsExempt_DoesNotReportViolation()
+ {
+ // Arrange.
+ const string pattern = "System.Xml";
+ var config = new ReferenceCopConfig();
+ config.Rules.Add(new ReferenceCopConfig.AssemblyName
+ {
+ Name = "NoXml",
+ Pattern = pattern,
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Error,
+ Exceptions = new List
+ {
+ new ReferenceCopConfig.ProjectException { Name = "ExemptProject" },
+ },
+ });
+
+ var detector = new AssemblyNameViolationDetector(new ExactMatchComparer(), config, "ExemptProject");
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(pattern)),
+ };
+
+ // Act.
+ var violations = detector.GetViolationsFromExperimental(references).ToList();
+
+ // Assert.
+ violations.Should().BeEmpty("because ExemptProject is in the rule's exceptions");
+ }
+ }
+}
diff --git a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
index 6bea93c..3ffbe94 100644
--- a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
+++ b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
@@ -39,6 +39,11 @@ public enum ViolationSeverity
Warning,
}
+ public Rule()
+ {
+ this.Exceptions = new List();
+ }
+
[XmlElement]
public string Name { get; set; }
@@ -47,6 +52,40 @@ public enum ViolationSeverity
[XmlElement]
public ViolationSeverity Severity { get; set; }
+
+ [XmlArray]
+ [XmlArrayItem("Project")]
+ public List Exceptions { get; set; }
+
+ ///
+ /// Determines whether the specified project is exempt from this rule.
+ ///
+ /// The project name to check.
+ /// True if the project is exempt; otherwise false.
+ public bool IsProjectExempt(string projectName)
+ {
+ if (string.IsNullOrEmpty(projectName) || this.Exceptions == null || this.Exceptions.Count == 0)
+ {
+ return false;
+ }
+
+ foreach (var exception in this.Exceptions)
+ {
+ if (string.Equals(exception.Name, projectName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ [Serializable]
+ public class ProjectException
+ {
+ [XmlAttribute]
+ public string Name { get; set; }
}
[Serializable]
diff --git a/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs b/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
index 009508e..aa3365b 100644
--- a/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
+++ b/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
@@ -11,11 +11,13 @@ public class AssemblyNameViolationDetector : IViolationDetector exactMatchRules;
private readonly List> patternRules;
private readonly IEqualityComparer referenceNameComparer;
+ private readonly string projectName;
- public AssemblyNameViolationDetector(IEqualityComparer referenceNameComparer, ReferenceCopConfig config)
+ public AssemblyNameViolationDetector(IEqualityComparer referenceNameComparer, ReferenceCopConfig config, string projectName = null)
{
this.rules = new Dictionary(referenceNameComparer);
this.referenceNameComparer = referenceNameComparer;
+ this.projectName = projectName;
// Separate exact matches from patterns for performance optimization.
this.exactMatchRules = new Dictionary(StringComparer.InvariantCulture);
@@ -44,6 +46,12 @@ public IEnumerable GetViolationsFrom(IEnumerable GetViolationsFromExperimental(IEnumerable GetViolationsFromExperimental(IEnumerable GetViolationsFrom(IEnumerable GetViolationsFrom(IEnumerable