Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions ReferenceCopConfig.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
<xs:element name="Description" type="xs:string" />
<xs:element name="Severity" type="xs:string" />
<xs:element name="Pattern" type="xs:string" />
<xs:element name="Exceptions" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Project" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="Name" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
Expand All @@ -26,6 +37,17 @@
<xs:element name="Severity" type="xs:string" />
<xs:element name="FromProjectTag" type="xs:string" />
<xs:element name="ToProjectTag" type="xs:string" />
<xs:element name="Exceptions" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Project" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="Name" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
Expand All @@ -37,6 +59,17 @@
<xs:element name="Severity" type="xs:string" />
<xs:element name="FromPath" type="xs:string" />
<xs:element name="ToPath" type="xs:string" />
<xs:element name="Exceptions" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="Project" maxOccurs="unbounded">
<xs:complexType>
<xs:attribute name="Name" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
Expand Down
2 changes: 1 addition & 1 deletion src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
175 changes: 175 additions & 0 deletions src/ReferenceCop.Tests/Detectors/ProjectExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -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<ReferenceCopConfig.ProjectException>
{
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<ReferenceCopConfig.ProjectException>
{
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<ReferenceCopConfig.ProjectException>
{
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<ReferenceCopConfig.ProjectException>
{
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<ReferenceCopConfig.ProjectException>
{
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<ReferenceCopConfig.ProjectException>
{
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");
}
}
}
39 changes: 39 additions & 0 deletions src/ReferenceCop/Configuration/ReferenceCopConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
Warning,
}

public Rule()

Check warning on line 42 in src/ReferenceCop/Configuration/ReferenceCopConfig.cs

View workflow job for this annotation

GitHub Actions / build

Check warning on line 42 in src/ReferenceCop/Configuration/ReferenceCopConfig.cs

View workflow job for this annotation

GitHub Actions / build

{
this.Exceptions = new List<ProjectException>();
}

[XmlElement]
public string Name { get; set; }

Expand All @@ -47,6 +52,40 @@

[XmlElement]
public ViolationSeverity Severity { get; set; }

[XmlArray]
[XmlArrayItem("Project")]
public List<ProjectException> Exceptions { get; set; }

/// <summary>
/// Determines whether the specified project is exempt from this rule.
/// </summary>
/// <param name="projectName">The project name to check.</param>
/// <returns>True if the project is exempt; otherwise false.</returns>
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]
Expand Down
20 changes: 17 additions & 3 deletions src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public class AssemblyNameViolationDetector : IViolationDetector<AssemblyIdentity
private readonly Dictionary<string, ReferenceCopConfig.Rule> exactMatchRules;
private readonly List<KeyValuePair<string, ReferenceCopConfig.Rule>> patternRules;
private readonly IEqualityComparer<string> referenceNameComparer;
private readonly string projectName;

public AssemblyNameViolationDetector(IEqualityComparer<string> referenceNameComparer, ReferenceCopConfig config)
public AssemblyNameViolationDetector(IEqualityComparer<string> referenceNameComparer, ReferenceCopConfig config, string projectName = null)
{
this.rules = new Dictionary<string, ReferenceCopConfig.Rule>(referenceNameComparer);
this.referenceNameComparer = referenceNameComparer;
this.projectName = projectName;

// Separate exact matches from patterns for performance optimization.
this.exactMatchRules = new Dictionary<string, ReferenceCopConfig.Rule>(StringComparer.InvariantCulture);
Expand Down Expand Up @@ -44,6 +46,12 @@ public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationC
continue;
}

// Check if this project is exempt from the rule
if (rule.Value.IsProjectExempt(this.projectName))
{
continue;
}

yield return new Violation(rule.Value, reference.Name);
}
}
Expand Down Expand Up @@ -78,15 +86,21 @@ public IEnumerable<Violation> GetViolationsFromExperimental(IEnumerable<Referenc
// Check exact match rules with O(1) lookup
if (this.exactMatchRules.TryGetValue(reference.Name, out var exactRule))
{
yield return new Violation(exactRule, reference.Name);
if (!exactRule.IsProjectExempt(this.projectName))
{
yield return new Violation(exactRule, reference.Name);
}
}

// Check pattern rules - only iterate through patterns (typically much smaller than total rules)
foreach (var patternRule in this.patternRules)
{
if (this.referenceNameComparer.Equals(patternRule.Key, reference.Name))
{
yield return new Violation(patternRule.Value, reference.Name);
if (!patternRule.Value.IsProjectExempt(this.projectName))
{
yield return new Violation(patternRule.Value, reference.Name);
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationC
continue;
}

// Check if this project is exempt from the rule
if (rule.IsProjectExempt(System.IO.Path.GetFileNameWithoutExtension(this.projectFilePath)))
{
continue;
}

yield return new Violation(rule, referenceContext.Reference);
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/ReferenceCop/Detectors/ProjectTagViolationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationC
continue;
}

// Check if this project is exempt from the rule
if (rule.IsProjectExempt(System.IO.Path.GetFileNameWithoutExtension(this.projectFilePath)))
{
continue;
}

yield return new Violation(rule, referenceContext.Reference);
}
}
Expand Down
Loading