diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 15b0525..365cc9b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -141,6 +141,13 @@ Ensure all required fields in the template are filled out appropriately when cre
- MSBuild integration in `ReferenceCopTask.cs`
- Roslyn integration in `ReferenceCopAnalyzer.cs`
+#### Adding Roslyn Diagnostics
+When adding new diagnostic descriptors for the Roslyn analyzer:
+1. Define the `DiagnosticDescriptor` in `DiagnosticDescriptors.cs` as a public static readonly field
+2. Register it in the `SupportedDiagnostics` property of `ReferenceCopAnalyzer.cs`
+3. Use `Diagnostic.Create(DiagnosticDescriptors.YourDescriptor, Location.None)` when reporting
+4. Never create inline `DiagnosticDescriptor` instances in analyzer code
+
### Code Generation and Modification Guidelines
When adding or modifying code in the ReferenceCop project, follow these guidelines:
diff --git a/.gitignore b/.gitignore
index 0cabefc..670991d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -147,6 +147,9 @@ L10N.Integrations/phantom*
# CAKE
tools/
+# BenchmarkDotNet artifacts
+BenchmarkDotNet.Artifacts/
+
# VS Code settings
.vscode/mcp.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6d1cd35..6fa76e4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,6 +23,25 @@ Thanks for checking out the project! We're just getting started, so any help wil
- Test your changes before submitting.
- Update docs if needed
+## Debugging
+
+To attach a debugger during development:
+
+1. **For MSBuild task debugging:**
+ - Add `MSBuild` to your project file
+ - Build the project - the debugger will launch automatically
+
+2. **For Roslyn analyzer debugging:**
+ - Add `Roslyn` to your project file
+ - Add `` to make the property visible to the analyzer
+ - Build the project - the debugger will launch automatically
+
+See examples in the playground project files for reference.
+
+## End-to-End Testing
+
+Use the `playground` directory for end-to-end validation of your changes. See the [playground README](playground/README.md) for detailed instructions on how to test ReferenceCop changes in a real project environment.
+
## Need help?
No worries! If you're stuck or have questions, just open an issue or create a discussion straight on GitHub :)
diff --git a/README.md b/README.md
index 5149496..24401db 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,42 @@ Here is an example of a `ReferenceCop.config` file:
In case you'd like to enable ReferenceCop and apply the same configuration to multiple projects in a directory, you can do that in a `Directory.Build.props` file by [Customizing the build by folder](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022).
+### Advanced Configuration
+
+ReferenceCop supports the following optional settings in the `ReferenceCop.config` file:
+
+#### UseExperimentalDetectors
+
+Enables experimental rule detectors that may provide additional validation capabilities. These detectors are under development and may change in future versions.
+
+**Default:** `false`
+
+**Example:**
+```xml
+
+ true
+
+
+
+
+```
+
+#### EnableDebugMessages
+
+Enables detailed debug messages during build and analysis, useful for troubleshooting rule evaluation and detector behavior.
+
+**Default:** `false`
+
+**Example:**
+```xml
+
+ true
+
+
+
+
+```
+
## License
ReferenceCop is licensed under the MIT license. See LICENSE for more information.
diff --git a/ReferenceCopConfig.xsd b/ReferenceCopConfig.xsd
index b3a06dc..96d0bcf 100644
--- a/ReferenceCopConfig.xsd
+++ b/ReferenceCopConfig.xsd
@@ -3,6 +3,8 @@
+
+
diff --git a/playground/.gitignore b/playground/.gitignore
index 0b61c43..19d3299 100644
--- a/playground/.gitignore
+++ b/playground/.gitignore
@@ -1,3 +1,5 @@
# Standard .NET ignores for the test projects
**/bin/
**/obj/
+# Local NuGet packages cache
+local-packages/
\ No newline at end of file
diff --git a/playground/TestProject/Directory.Build.props b/playground/TestProject/Directory.Build.props
index 965c080..ec9c640 100644
--- a/playground/TestProject/Directory.Build.props
+++ b/playground/TestProject/Directory.Build.props
@@ -30,4 +30,8 @@
+
+
+
+
diff --git a/playground/TestProject/ReferenceCop.config b/playground/TestProject/ReferenceCop.config
index 27a7169..dce9c77 100644
--- a/playground/TestProject/ReferenceCop.config
+++ b/playground/TestProject/ReferenceCop.config
@@ -1,5 +1,7 @@
+ true
+ true
diff --git a/playground/TestProject/SampleApp/SampleApp.csproj b/playground/TestProject/SampleApp/SampleApp.csproj
index bba6818..a1fec65 100644
--- a/playground/TestProject/SampleApp/SampleApp.csproj
+++ b/playground/TestProject/SampleApp/SampleApp.csproj
@@ -6,8 +6,17 @@
enable
enable
App
+
+
+
+
+
+
+
+
+
diff --git a/playground/TestProject/SampleLibrary/SampleLibrary.csproj b/playground/TestProject/SampleLibrary/SampleLibrary.csproj
index 1cbd80b..1f88f55 100644
--- a/playground/TestProject/SampleLibrary/SampleLibrary.csproj
+++ b/playground/TestProject/SampleLibrary/SampleLibrary.csproj
@@ -6,10 +6,18 @@
enable
Library
+
+
+
+
+
-
+
+
+
+
diff --git a/playground/TestProject/nuget.config b/playground/nuget.config
similarity index 65%
rename from playground/TestProject/nuget.config
rename to playground/nuget.config
index 8418d88..f45e370 100644
--- a/playground/TestProject/nuget.config
+++ b/playground/nuget.config
@@ -1,9 +1,13 @@
+
+
+
+
-
+
diff --git a/playground/playground.slnx b/playground/playground.slnx
new file mode 100644
index 0000000..c5ee38d
--- /dev/null
+++ b/playground/playground.slnx
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 194152f..9c450d8 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -1,5 +1,6 @@
+
diff --git a/src/ReferenceCop.Benchmarks/AssemblyNameViolationDetectorBenchmarks.cs b/src/ReferenceCop.Benchmarks/AssemblyNameViolationDetectorBenchmarks.cs
new file mode 100644
index 0000000..a8fa489
--- /dev/null
+++ b/src/ReferenceCop.Benchmarks/AssemblyNameViolationDetectorBenchmarks.cs
@@ -0,0 +1,104 @@
+namespace ReferenceCop.Benchmarks
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using BenchmarkDotNet.Attributes;
+ using Microsoft.CodeAnalysis;
+
+ [MemoryDiagnoser]
+ [RankColumn]
+ public class AssemblyNameViolationDetectorBenchmarks
+ {
+ private AssemblyNameViolationDetector detector;
+ private List> references;
+
+ [Params(10, 50, 100)]
+ public int RuleCount { get; set; }
+
+ [Params(10, 100, 500)]
+ public int ReferenceCount { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var config = this.CreateConfig(this.RuleCount);
+ this.detector = new AssemblyNameViolationDetector(new PatternMatchComparer(), config);
+ this.references = this.CreateReferences(this.ReferenceCount);
+ }
+
+ [Benchmark(Baseline = true)]
+ public int Original()
+ {
+ return this.detector.GetViolationsFrom(this.references).Count();
+ }
+
+ [Benchmark]
+ public int Experimental()
+ {
+ return this.detector.GetViolationsFromExperimental(this.references).Count();
+ }
+
+ private ReferenceCopConfig CreateConfig(int ruleCount)
+ {
+ var config = new ReferenceCopConfig();
+ var rules = new List();
+
+ // Create a mix of exact match and pattern rules (70% exact, 30% patterns)
+ for (int i = 0; i < ruleCount; i++)
+ {
+ string pattern;
+ if (i % 10 < 7)
+ {
+ // Exact match
+ pattern = $"ExactMatch.Assembly{i}";
+ }
+ else
+ {
+ // Pattern match
+ pattern = $"Pattern.Assembly{i}.*";
+ }
+
+ rules.Add(new ReferenceCopConfig.AssemblyName
+ {
+ Pattern = pattern,
+ Severity = ReferenceCopConfig.Rule.ViolationSeverity.Warning,
+ Description = $"Reference to {pattern} is discouraged",
+ });
+ }
+
+ config.Rules = rules;
+ return config;
+ }
+
+ private List> CreateReferences(int count)
+ {
+ var references = new List>();
+
+ for (int i = 0; i < count; i++)
+ {
+ string assemblyName;
+
+ // Create a mix where some match rules and some don't
+ if (i % 5 == 0)
+ {
+ // Exact match (will match exact rules)
+ assemblyName = $"ExactMatch.Assembly{i % this.RuleCount}";
+ }
+ else if (i % 5 == 1)
+ {
+ // Pattern match (will match pattern rules)
+ assemblyName = $"Pattern.Assembly{i % this.RuleCount}.SubAssembly";
+ }
+ else
+ {
+ // No match
+ assemblyName = $"NoMatch.Assembly{i}";
+ }
+
+ references.Add(ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(assemblyName)));
+ }
+
+ return references;
+ }
+ }
+}
diff --git a/src/ReferenceCop.Benchmarks/Program.cs b/src/ReferenceCop.Benchmarks/Program.cs
new file mode 100644
index 0000000..051b91b
--- /dev/null
+++ b/src/ReferenceCop.Benchmarks/Program.cs
@@ -0,0 +1,12 @@
+namespace ReferenceCop.Benchmarks
+{
+ using BenchmarkDotNet.Running;
+
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BenchmarkRunner.Run();
+ }
+ }
+}
diff --git a/src/ReferenceCop.Benchmarks/README.md b/src/ReferenceCop.Benchmarks/README.md
new file mode 100644
index 0000000..1dd9b88
--- /dev/null
+++ b/src/ReferenceCop.Benchmarks/README.md
@@ -0,0 +1,27 @@
+# ReferenceCop Benchmarks
+
+This project contains performance benchmarks for the ReferenceCop library using BenchmarkDotNet.
+
+## Running the Benchmarks
+
+To run the benchmarks, execute the following command from the repository root:
+
+```powershell
+dotnet run -c Release --project src/ReferenceCop.Benchmarks/ReferenceCop.Benchmarks.csproj
+```
+
+## AssemblyNameViolationDetectorBenchmarks
+
+Compares the performance of the original `GetViolationsFrom` method against the optimized `GetViolationsFromExperimental` method.
+
+## Understanding the Results
+
+BenchmarkDotNet will display:
+- **Mean**: Average execution time
+- **Error**: Standard error of all measurements
+- **StdDev**: Standard deviation of all measurements
+- **Rank**: Relative ranking of methods
+- **Gen0/1/2**: Garbage collection statistics
+- **Allocated**: Total memory allocated
+
+Look for the **Rank** column and **Mean** times to compare performance.
diff --git a/src/ReferenceCop.Benchmarks/ReferenceCop.Benchmarks.csproj b/src/ReferenceCop.Benchmarks/ReferenceCop.Benchmarks.csproj
new file mode 100644
index 0000000..4c2ead2
--- /dev/null
+++ b/src/ReferenceCop.Benchmarks/ReferenceCop.Benchmarks.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs b/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs
index 4e99f01..4648b9e 100644
--- a/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs
+++ b/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs
@@ -27,6 +27,22 @@ public static void LogViolation(this IBuildEngine self, Violation violation, str
}
}
+ public static void LogDebugMessage(this IBuildEngine self, string message)
+ {
+ var warningEvent = new BuildWarningEventArgs(
+ subcategory: SenderName,
+ code: "RC9999",
+ file: default,
+ lineNumber: default,
+ columnNumber: default,
+ endLineNumber: default,
+ endColumnNumber: default,
+ message: $"[DEBUG]: {message}",
+ helpKeyword: default,
+ senderName: SenderName);
+ self.LogWarningEvent(warningEvent);
+ }
+
internal static void LogErrorEvent(this IBuildEngine self, Exception ex)
{
var errorEvent = CreateErrorEventFor(ex);
diff --git a/src/ReferenceCop.MSBuild/ReferenceCopTask.cs b/src/ReferenceCop.MSBuild/ReferenceCopTask.cs
index 836d16a..d370247 100644
--- a/src/ReferenceCop.MSBuild/ReferenceCopTask.cs
+++ b/src/ReferenceCop.MSBuild/ReferenceCopTask.cs
@@ -73,13 +73,22 @@ public bool Execute()
var configLoader = this.configLoaderFactory(configFilePath);
var config = configLoader.Load();
+ if (config.EnableDebugMessages && config.UseExperimentalDetectors)
+ {
+ this.BuildEngine.LogDebugMessage("Using experimental detectors");
+ }
+
var projectReferences = this.projectReferencesProvider.GetProjectReferences(this.ProjectFile.ItemSpec);
var evaluationContexts = projectReferences
.Select(_ => ReferenceEvaluationContextFactory.Create(_.Path, _.NoWarn))
.ToList();
var projectTagViolationDetector = this.projectTagViolationDetectorFactory(config, this.ProjectFile.ItemSpec);
- foreach (var violation in projectTagViolationDetector.GetViolationsFrom(evaluationContexts))
+ var projectTagViolations = config.UseExperimentalDetectors
+ ? projectTagViolationDetector.GetViolationsFromExperimental(evaluationContexts)
+ : projectTagViolationDetector.GetViolationsFrom(evaluationContexts);
+
+ foreach (var violation in projectTagViolations)
{
if (violation.Rule.Severity == ReferenceCopConfig.Rule.ViolationSeverity.Error)
{
@@ -92,7 +101,11 @@ public bool Execute()
var repositoryRoot = this.projectReferencesProvider.GetPropertyValue(
this.ProjectFile.ItemSpec, ReferenceCopRepositoryRootProperty);
var projectPathViolationDetector = this.projectPathViolationDetectorFactory(config, this.ProjectFile.ItemSpec, repositoryRoot);
- foreach (var violation in projectPathViolationDetector.GetViolationsFrom(evaluationContexts))
+ var projectPathViolations = config.UseExperimentalDetectors
+ ? projectPathViolationDetector.GetViolationsFromExperimental(evaluationContexts)
+ : projectPathViolationDetector.GetViolationsFrom(evaluationContexts);
+
+ foreach (var violation in projectPathViolations)
{
if (violation.Rule.Severity == ReferenceCopConfig.Rule.ViolationSeverity.Error)
{
diff --git a/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs b/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs
index 8413df7..2c44ad5 100644
--- a/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs
+++ b/src/ReferenceCop.Roslyn/DiagnosticDescriptors.cs
@@ -28,6 +28,14 @@ internal class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor DebugMessage = new DiagnosticDescriptor(
+ "RC9999",
+ "Debug Message",
+ "[DEBUG]: {0}",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
private const string Category = "ReferenceCop";
}
}
diff --git a/src/ReferenceCop.Roslyn/DiagnosticFactory.cs b/src/ReferenceCop.Roslyn/DiagnosticFactory.cs
index 535e8a2..7f4b815 100644
--- a/src/ReferenceCop.Roslyn/DiagnosticFactory.cs
+++ b/src/ReferenceCop.Roslyn/DiagnosticFactory.cs
@@ -35,5 +35,13 @@ public static Diagnostic CreateFor(Exception ex)
Location.None,
ex.Message);
}
+
+ public static Diagnostic CreateDebugMessage(string message)
+ {
+ return Diagnostic.Create(
+ DiagnosticDescriptors.DebugMessage,
+ Location.None,
+ message);
+ }
}
}
diff --git a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
index 25e9665..81fb862 100644
--- a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
+++ b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
@@ -16,6 +16,7 @@ public class ReferenceCopAnalyzer : DiagnosticAnalyzer
private readonly INoWarnAssembliesProvider noWarnAssembliesProvider;
private IViolationDetector assemblyNameViolationDetector;
+ private ReferenceCopConfig config;
public ReferenceCopAnalyzer()
{
@@ -25,7 +26,8 @@ public ReferenceCopAnalyzer()
public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(
DiagnosticDescriptors.GeneralError,
DiagnosticDescriptors.IllegalReferenceRule,
- DiagnosticDescriptors.DiscouragedReferenceRule);
+ DiagnosticDescriptors.DiscouragedReferenceRule,
+ DiagnosticDescriptors.DebugMessage);
public override void Initialize(AnalysisContext context)
{
@@ -39,8 +41,15 @@ public override void Initialize(AnalysisContext context)
try
{
var configLoader = new XmlConfigurationLoader(compilationAnalysisContext);
- var config = configLoader.Load();
- this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), config);
+ this.config = configLoader.Load();
+ this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), this.config);
+
+ if (this.config.EnableDebugMessages && this.config.UseExperimentalDetectors)
+ {
+ compilationAnalysisContext.ReportDiagnostic(
+ DiagnosticFactory.CreateDebugMessage("Using experimental detectors"));
+ }
+
this.AnalyzeCompilation(compilationAnalysisContext);
}
catch (Exception ex)
@@ -75,7 +84,11 @@ private void AnalyzeCompilation(CompilationAnalysisContext compilationAnalysisCo
})
.ToList();
- foreach (var violation in this.assemblyNameViolationDetector.GetViolationsFrom(evaluationContexts))
+ var violations = this.config.UseExperimentalDetectors
+ ? this.assemblyNameViolationDetector.GetViolationsFromExperimental(evaluationContexts)
+ : this.assemblyNameViolationDetector.GetViolationsFrom(evaluationContexts);
+
+ foreach (var violation in violations)
{
compilationAnalysisContext.ReportDiagnostic(DiagnosticFactory.CreateFor(violation));
}
diff --git a/src/ReferenceCop.Tests/Configuration/ReferenceCopConfigTests.cs b/src/ReferenceCop.Tests/Configuration/ReferenceCopConfigTests.cs
new file mode 100644
index 0000000..eabd54f
--- /dev/null
+++ b/src/ReferenceCop.Tests/Configuration/ReferenceCopConfigTests.cs
@@ -0,0 +1,34 @@
+namespace ReferenceCop.Tests
+{
+ using FluentAssertions;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class ReferenceCopConfigTests
+ {
+ [TestMethod]
+ public void Constructor_SetsDefaultValues()
+ {
+ // Act.
+ var config = new ReferenceCopConfig();
+
+ // Assert.
+ config.Rules.Should().NotBeNull();
+ config.Rules.Should().BeEmpty();
+ config.UseExperimentalDetectors.Should().BeFalse("default value should be false for backward compatibility");
+ }
+
+ [TestMethod]
+ public void UseExperimentalDetectors_CanBeSet()
+ {
+ // Arrange.
+ var config = new ReferenceCopConfig();
+
+ // Act.
+ config.UseExperimentalDetectors = true;
+
+ // Assert.
+ config.UseExperimentalDetectors.Should().BeTrue();
+ }
+ }
+}
diff --git a/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs b/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs
index 218a81d..e19f650 100644
--- a/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs
+++ b/src/ReferenceCop.Tests/Detectors/AssemblyNameViolationDetectorTests.cs
@@ -221,5 +221,213 @@ public void GetViolationsFrom_WhenNullReference_ThrowsInvalidOperationException(
// Assert
act.Should().Throw();
}
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenNoRules_ReturnsEmpty()
+ {
+ // Arrange.
+ var config = new ReferenceCopConfig();
+ var detector = new AssemblyNameViolationDetector(Substitute.For>(), config);
+
+ // Act.
+ var result = detector.GetViolationsFromExperimental(new List>
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Xml.Linq")),
+ });
+
+ // Assert.
+ result.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenMatchingRule_ReturnsViolation()
+ {
+ // Arrange.
+ const string detectableValue = "System.Xml";
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule(detectableValue)
+ .Build();
+ var comparer = Substitute.For>();
+ comparer.Equals(detectableValue, detectableValue).Returns(true);
+ var detector = new AssemblyNameViolationDetector(comparer, config);
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(detectableValue)),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Xml.Serialization")),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Xml.Linq")),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references);
+
+ // Assert
+ diagnostics.Should().HaveCount(1);
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenMatchingRuleUsesPatternMatch_ReturnsViolations()
+ {
+ // Arrange.
+ const string partialMatch = "System.Xml";
+ var detectablePattern = $"{partialMatch}.*";
+ var detectableValue1 = $"{partialMatch}.Serialization";
+ var detectableValue2 = $"{partialMatch}.Linq";
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule(detectablePattern)
+ .Build();
+ var detector = new AssemblyNameViolationDetector(new PatternMatchComparer(), config);
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(detectableValue1)),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(detectableValue2)),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Text")),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references);
+
+ // Assert
+ diagnostics.Should().HaveCount(2);
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenNoMatchingRules_ReturnsEmpty()
+ {
+ // Arrange.
+ var config = new ReferenceCopConfigBuilder().Build();
+ var detector = new AssemblyNameViolationDetector(Substitute.For>(), config);
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Xml.Serialization")),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Xml.Linq")),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references);
+
+ // Assert
+ diagnostics.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenNoReferences_ReturnsEmpty()
+ {
+ // Arrange.
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule("somePattern")
+ .Build();
+ var detector = new AssemblyNameViolationDetector(Substitute.For>(), config);
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(Array.Empty>());
+
+ // Assert
+ diagnostics.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenWarningSuppressed_SkipsViolation()
+ {
+ // Arrange.
+ const string detectableValue = "System.Xml";
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule(detectableValue)
+ .Build();
+ var comparer = Substitute.For>();
+ comparer.Equals(detectableValue, detectableValue).Returns(true);
+ var detector = new AssemblyNameViolationDetector(comparer, config);
+ var references = new[]
+ {
+ // Create with warning suppressed
+ new ReferenceEvaluationContext(new AssemblyIdentity(detectableValue), isWarningSuppressed: true),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references);
+
+ // Assert
+ diagnostics.Should().BeEmpty("because the violation was suppressed");
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenMultipleReferencesWithSomeSuppressed_ReturnsSomeViolations()
+ {
+ // Arrange.
+ const string partialMatch = "System.Xml";
+ var detectablePattern = $"{partialMatch}.*";
+ var detectableValue1 = $"{partialMatch}.Serialization";
+ var detectableValue2 = $"{partialMatch}.Linq";
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule(detectablePattern)
+ .Build();
+ var detector = new AssemblyNameViolationDetector(new PatternMatchComparer(), config);
+ var references = new[]
+ {
+ // Regular reference with no suppression
+ new ReferenceEvaluationContext(new AssemblyIdentity(detectableValue1), isWarningSuppressed: false),
+
+ // Suppressed reference
+ new ReferenceEvaluationContext(new AssemblyIdentity(detectableValue2), isWarningSuppressed: true),
+
+ // Non-matching reference
+ new ReferenceEvaluationContext(new AssemblyIdentity("System.Text"), isWarningSuppressed: false),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references);
+
+ // Assert
+ diagnostics.Should().ContainSingle()
+ .Which.ReferenceName.Should().Be(detectableValue1);
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenNullReference_ThrowsInvalidOperationException()
+ {
+ // Arrange.
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule("somePattern")
+ .Build();
+ var detector = new AssemblyNameViolationDetector(Substitute.For>(), config);
+ var references = new ReferenceEvaluationContext[]
+ {
+ ReferenceEvaluationContextFactory.Create(null),
+ };
+
+ // Act
+ Action act = () => detector.GetViolationsFromExperimental(references).First();
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [TestMethod]
+ public void GetViolationsFromExperimental_WhenMixedExactAndPatternRules_ReturnsCorrectViolations()
+ {
+ // Arrange.
+ const string exactMatch = "System.Xml";
+ const string pattern = "Microsoft.*";
+ var config = new ReferenceCopConfigBuilder()
+ .WithAssemblyNameRule(exactMatch)
+ .WithAssemblyNameRule(pattern)
+ .Build();
+ var detector = new AssemblyNameViolationDetector(new PatternMatchComparer(), config);
+ var references = new[]
+ {
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity(exactMatch)),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("Microsoft.CodeAnalysis")),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("Microsoft.Build")),
+ ReferenceEvaluationContextFactory.Create(new AssemblyIdentity("System.Text")),
+ };
+
+ // Act
+ var diagnostics = detector.GetViolationsFromExperimental(references).ToList();
+
+ // Assert
+ diagnostics.Should().HaveCount(3);
+ diagnostics.Should().Contain(v => v.ReferenceName == exactMatch);
+ diagnostics.Should().Contain(v => v.ReferenceName == "Microsoft.CodeAnalysis");
+ diagnostics.Should().Contain(v => v.ReferenceName == "Microsoft.Build");
+ }
}
}
diff --git a/src/ReferenceCop.sln b/src/ReferenceCop.sln
index 9603185..45cf85e 100644
--- a/src/ReferenceCop.sln
+++ b/src/ReferenceCop.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.8.34408.163
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11222.15
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferenceCop", "ReferenceCop\ReferenceCop.csproj", "{2201B4F1-5882-44C6-944B-E661A504BA8B}"
EndProject
@@ -44,44 +44,126 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReferenceCop.MSBuild.Tests", "ReferenceCop.MSBuild.Tests\ReferenceCop.MSBuild.Tests.csproj", "{6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReferenceCop.Benchmarks", "ReferenceCop.Benchmarks\ReferenceCop.Benchmarks.csproj", "{02E3E256-3CF4-4386-83F0-7E2CAEC6C310}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|x64.Build.0 = Debug|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Debug|x86.Build.0 = Debug|Any CPU
{2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|x64.ActiveCfg = Release|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|x64.Build.0 = Release|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|x86.ActiveCfg = Release|Any CPU
+ {2201B4F1-5882-44C6-944B-E661A504BA8B}.Release|x86.Build.0 = Release|Any CPU
{5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|x64.Build.0 = Debug|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Debug|x86.Build.0 = Debug|Any CPU
{5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|x64.ActiveCfg = Release|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|x64.Build.0 = Release|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|x86.ActiveCfg = Release|Any CPU
+ {5068AD04-AD6C-4F6F-BF73-4EB6E3941D05}.Release|x86.Build.0 = Release|Any CPU
{A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|x64.Build.0 = Debug|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Debug|x86.Build.0 = Debug|Any CPU
{A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|x64.ActiveCfg = Release|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|x64.Build.0 = Release|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|x86.ActiveCfg = Release|Any CPU
+ {A0EAF318-32D4-4545-B0F2-A65F5A10DECA}.Release|x86.Build.0 = Release|Any CPU
{272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|x64.Build.0 = Debug|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Debug|x86.Build.0 = Debug|Any CPU
{272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|x64.ActiveCfg = Release|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|x64.Build.0 = Release|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|x86.ActiveCfg = Release|Any CPU
+ {272C305F-3638-4F57-AAE5-3C79E4FDB96E}.Release|x86.Build.0 = Release|Any CPU
{1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|x64.Build.0 = Debug|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Debug|x86.Build.0 = Debug|Any CPU
{1DB93B03-1F4A-457F-92DC-03B535849112}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DB93B03-1F4A-457F-92DC-03B535849112}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Release|x64.ActiveCfg = Release|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Release|x64.Build.0 = Release|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Release|x86.ActiveCfg = Release|Any CPU
+ {1DB93B03-1F4A-457F-92DC-03B535849112}.Release|x86.Build.0 = Release|Any CPU
{303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|x64.Build.0 = Debug|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Debug|x86.Build.0 = Debug|Any CPU
{303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|Any CPU.Build.0 = Release|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|x64.ActiveCfg = Release|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|x64.Build.0 = Release|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|x86.ActiveCfg = Release|Any CPU
+ {303D7A36-0E10-4DB9-BF0B-9D919698B012}.Release|x86.Build.0 = Release|Any CPU
{06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|x64.Build.0 = Debug|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Debug|x86.Build.0 = Debug|Any CPU
{06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|x64.ActiveCfg = Release|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|x64.Build.0 = Release|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|x86.ActiveCfg = Release|Any CPU
+ {06921A6C-BA8A-43B3-9458-50470FB0EAAA}.Release|x86.Build.0 = Release|Any CPU
{6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|x64.Build.0 = Debug|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Debug|x86.Build.0 = Debug|Any CPU
{6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|x64.ActiveCfg = Release|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|x64.Build.0 = Release|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|x86.ActiveCfg = Release|Any CPU
+ {6A6D30A1-B0B9-409D-A5CF-BA414ACE0491}.Release|x86.Build.0 = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|x64.Build.0 = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Debug|x86.Build.0 = Debug|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|Any CPU.Build.0 = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|x64.ActiveCfg = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|x64.Build.0 = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|x86.ActiveCfg = Release|Any CPU
+ {02E3E256-3CF4-4386-83F0-7E2CAEC6C310}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
index d23533c..6bea93c 100644
--- a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
+++ b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs
@@ -11,8 +11,16 @@ public class ReferenceCopConfig
public ReferenceCopConfig()
{
this.Rules = new List();
+ this.UseExperimentalDetectors = false;
+ this.EnableDebugMessages = false;
}
+ [XmlElement]
+ public bool UseExperimentalDetectors { get; set; }
+
+ [XmlElement]
+ public bool EnableDebugMessages { get; set; }
+
[XmlArrayItem(typeof(AssemblyName))]
[XmlArrayItem(typeof(ProjectTag))]
[XmlArrayItem(typeof(ProjectPath))]
diff --git a/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs b/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
index 14b58d8..009508e 100644
--- a/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
+++ b/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
@@ -8,13 +8,20 @@
public class AssemblyNameViolationDetector : IViolationDetector
{
private readonly Dictionary rules;
+ private readonly Dictionary exactMatchRules;
+ private readonly List> patternRules;
private readonly IEqualityComparer referenceNameComparer;
public AssemblyNameViolationDetector(IEqualityComparer referenceNameComparer, ReferenceCopConfig config)
{
this.rules = new Dictionary(referenceNameComparer);
- this.LoadRulesFrom(config);
this.referenceNameComparer = referenceNameComparer;
+
+ // Separate exact matches from patterns for performance optimization.
+ this.exactMatchRules = new Dictionary(StringComparer.InvariantCulture);
+ this.patternRules = new List>();
+
+ this.LoadRulesFrom(config);
}
public IEnumerable GetViolationsFrom(IEnumerable> references)
@@ -43,11 +50,60 @@ public IEnumerable GetViolationsFrom(IEnumerable
+ /// Optimized O(n) version of GetViolationsFrom for PatternMatchComparer.
+ /// Separates exact match rules from wildcard patterns for improved performance.
+ /// Exact matches use O(1) dictionary lookup, while patterns use O(n*p) iteration where p is pattern count.
+ /// Total complexity: O(n) for exact matches only, O(n*p) when patterns exist (much better than O(n*m) where m = total rules).
+ ///
+ /// The references to evaluate.
+ /// Violations found in the references.
+ public IEnumerable GetViolationsFromExperimental(IEnumerable> references)
+ {
+ // O(n) iteration through references, with O(1) lookup for exact matches
+ foreach (var referenceContext in references)
+ {
+ var reference = referenceContext.Reference;
+ if (string.IsNullOrEmpty(reference?.Name))
+ {
+ throw new InvalidOperationException("Reference name cannot be null or empty.");
+ }
+
+ // Skip if warning is suppressed
+ if (referenceContext.IsWarningSuppressed)
+ {
+ continue;
+ }
+
+ // Check exact match rules with O(1) lookup
+ if (this.exactMatchRules.TryGetValue(reference.Name, out var exactRule))
+ {
+ 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);
+ }
+ }
+ }
+ }
+
+ private static bool IsExactMatch(string pattern)
+ {
+ // A pattern is exact if it doesn't contain wildcards and isn't the default "*" pattern
+ return pattern != "*" && !pattern.Contains('*');
+ }
+
private void LoadRulesFrom(ReferenceCopConfig config)
{
var assemblyNameRules = config.Rules.OfType();
foreach (var rule in assemblyNameRules)
{
+ // Load into original dictionary for backward compatibility
try
{
this.rules.Add(rule.Pattern, rule);
@@ -56,6 +112,16 @@ private void LoadRulesFrom(ReferenceCopConfig config)
{
throw new InvalidOperationException($"Duplicate rule pattern '{rule.Pattern}' found in the configuration file.");
}
+
+ // Also load into optimized structures - separate exact matches from patterns
+ if (IsExactMatch(rule.Pattern))
+ {
+ this.exactMatchRules.Add(rule.Pattern, rule);
+ }
+ else
+ {
+ this.patternRules.Add(new KeyValuePair(rule.Pattern, rule));
+ }
}
}
}
diff --git a/src/ReferenceCop/Detectors/IViolationDetector.cs b/src/ReferenceCop/Detectors/IViolationDetector.cs
index 81fb561..3884884 100644
--- a/src/ReferenceCop/Detectors/IViolationDetector.cs
+++ b/src/ReferenceCop/Detectors/IViolationDetector.cs
@@ -5,5 +5,7 @@
public interface IViolationDetector
{
IEnumerable GetViolationsFrom(IEnumerable> references);
+
+ IEnumerable GetViolationsFromExperimental(IEnumerable> references);
}
}
diff --git a/src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs b/src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs
index 588ebe2..bd522e5 100644
--- a/src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs
+++ b/src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs
@@ -44,6 +44,11 @@ public IEnumerable GetViolationsFrom(IEnumerable GetViolationsFromExperimental(IEnumerable> references)
+ {
+ return this.GetViolationsFrom(references);
+ }
+
private void LoadRulesFrom(ReferenceCopConfig config)
{
var projectPathRules = config.Rules.OfType();
diff --git a/src/ReferenceCop/Detectors/ProjectTagViolationDetector.cs b/src/ReferenceCop/Detectors/ProjectTagViolationDetector.cs
index 8863b7c..a2e4062 100644
--- a/src/ReferenceCop/Detectors/ProjectTagViolationDetector.cs
+++ b/src/ReferenceCop/Detectors/ProjectTagViolationDetector.cs
@@ -44,6 +44,11 @@ public IEnumerable GetViolationsFrom(IEnumerable GetViolationsFromExperimental(IEnumerable> references)
+ {
+ return this.GetViolationsFrom(references);
+ }
+
private void LoadRulesFrom(ReferenceCopConfig config)
{
var projectTagRules = config.Rules.OfType();