From 8e7aa81125029acc54ce94a38ec55c75cf6416b6 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:05:49 +0100 Subject: [PATCH 1/8] Add optimized AssemblyNameViolationDetector version Also includes config flag to switch on and to enable debug messages --- src/Directory.Packages.props | 1 + .../Configuration/ReferenceCopConfig.cs | 8 +++ .../AssemblyNameViolationDetector.cs | 68 ++++++++++++++++++- .../Detectors/IViolationDetector.cs | 2 + .../Detectors/ProjectPathViolationDetector.cs | 5 ++ .../Detectors/ProjectTagViolationDetector.cs | 5 ++ 6 files changed, 88 insertions(+), 1 deletion(-) 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/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(); From e5509af9f02edf22d81dfe6cd8e10d34b20bf599 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:10:25 +0100 Subject: [PATCH 2/8] Uptake experimental detectors in Roslyn Analyzer --- .../DiagnosticDescriptors.cs | 8 +++++++ src/ReferenceCop.Roslyn/DiagnosticFactory.cs | 8 +++++++ .../ReferenceCopAnalyzer.cs | 21 +++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) 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)); } From 03006576a1a8317144e2900c6922ca1db2e13385 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:10:40 +0100 Subject: [PATCH 3/8] Add unit tests --- .../AssemblyNameViolationDetectorTests.cs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) 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"); + } } } From 86fbb14225b368a60a8a8127dbff3fae9c127266 Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:11:33 +0100 Subject: [PATCH 4/8] Uptake experimental detectors in MSBuild Task --- .../BuildEngineExtensions.cs | 16 ++++++++++++++++ src/ReferenceCop.MSBuild/ReferenceCopTask.cs | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) 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) { From 841897ac4ba74fa22ac27f85324748d70364f42a Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:11:47 +0100 Subject: [PATCH 5/8] Update configuration schema --- ReferenceCopConfig.xsd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReferenceCopConfig.xsd b/ReferenceCopConfig.xsd index b3a06dc..96d0bcf 100644 --- a/ReferenceCopConfig.xsd +++ b/ReferenceCopConfig.xsd @@ -3,6 +3,8 @@ + + From 2107d4958de8b517a46bdfe6bc88e7e2ba7255fa Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:12:29 +0100 Subject: [PATCH 6/8] Add benchmarks for comparing original vs experimental --- .gitignore | 3 + ...AssemblyNameViolationDetectorBenchmarks.cs | 104 ++++++++++++++++++ src/ReferenceCop.Benchmarks/Program.cs | 12 ++ src/ReferenceCop.Benchmarks/README.md | 27 +++++ .../ReferenceCop.Benchmarks.csproj | 17 +++ .../Configuration/ReferenceCopConfigTests.cs | 34 ++++++ src/ReferenceCop.sln | 86 ++++++++++++++- 7 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 src/ReferenceCop.Benchmarks/AssemblyNameViolationDetectorBenchmarks.cs create mode 100644 src/ReferenceCop.Benchmarks/Program.cs create mode 100644 src/ReferenceCop.Benchmarks/README.md create mode 100644 src/ReferenceCop.Benchmarks/ReferenceCop.Benchmarks.csproj create mode 100644 src/ReferenceCop.Tests/Configuration/ReferenceCopConfigTests.cs 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/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.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.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 From 10745c8aa8c56f530d1092a61f0e6f7330bc1dec Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:12:48 +0100 Subject: [PATCH 7/8] Improve playground integration for validation --- playground/.gitignore | 2 ++ playground/TestProject/Directory.Build.props | 4 ++++ playground/TestProject/ReferenceCop.config | 2 ++ playground/TestProject/SampleApp/SampleApp.csproj | 9 +++++++++ .../TestProject/SampleLibrary/SampleLibrary.csproj | 10 +++++++++- playground/{TestProject => }/nuget.config | 6 +++++- playground/playground.slnx | 10 ++++++++++ 7 files changed, 41 insertions(+), 2 deletions(-) rename playground/{TestProject => }/nuget.config (65%) create mode 100644 playground/playground.slnx 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 @@ + + + + + + + + + + From 8d23a58aeb7a1c566af035671629c1141489af1c Mon Sep 17 00:00:00 2001 From: Marco Fogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:14:54 +0100 Subject: [PATCH 8/8] Update docs and instructions --- .github/copilot-instructions.md | 7 +++++++ CONTRIBUTING.md | 19 +++++++++++++++++ README.md | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) 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/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.