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