From d0f178231c281b6509b31983c1de5974e995349e Mon Sep 17 00:00:00 2001 From: mfogliatto <2962955+mfogliatto@users.noreply.github.com> Date: Sun, 31 May 2026 08:07:55 +0000 Subject: [PATCH] fix: introduce tracing capabilities for debugging rule triggers Adds an ITraceWriter abstraction with NullTraceWriter (disabled) and TraceWriter (enabled) implementations. When EnableTracing is set to true in the ReferenceCop config, all violation detectors emit detailed trace messages explaining rule evaluation: which rules match, which references are evaluated, why violations are triggered or suppressed. - MSBuild task: trace messages are flushed to the build log as normal importance messages with [TRACE] prefix - Roslyn analyzer: trace messages are reported as debug diagnostics - Performance: zero overhead when tracing is disabled (NullTraceWriter short-circuits via IsEnabled check) - Backward compatible: all existing constructors preserved Fixes mfogliatto/ReferenceCop#31 --- ReferenceCopConfig.xsd | 1 + .../BuildEngineExtensions.cs | 10 ++++ src/ReferenceCop.MSBuild/ReferenceCopTask.cs | 59 ++++++++++++++++--- .../ReferenceCopAnalyzer.cs | 16 ++++- .../Configuration/ReferenceCopConfig.cs | 4 ++ .../AssemblyNameViolationDetector.cs | 37 ++++++++++++ .../Detectors/ProjectPathViolationDetector.cs | 36 +++++++++++ .../Detectors/ProjectTagViolationDetector.cs | 36 +++++++++++ src/ReferenceCop/Tracing/ITraceWriter.cs | 19 ++++++ src/ReferenceCop/Tracing/NullTraceWriter.cs | 17 ++++++ src/ReferenceCop/Tracing/TraceWriter.cs | 40 +++++++++++++ 11 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 src/ReferenceCop/Tracing/ITraceWriter.cs create mode 100644 src/ReferenceCop/Tracing/NullTraceWriter.cs create mode 100644 src/ReferenceCop/Tracing/TraceWriter.cs diff --git a/ReferenceCopConfig.xsd b/ReferenceCopConfig.xsd index 96d0bcf..a3eb8d2 100644 --- a/ReferenceCopConfig.xsd +++ b/ReferenceCopConfig.xsd @@ -5,6 +5,7 @@ + diff --git a/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs b/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs index 4648b9e..faf7720 100644 --- a/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs +++ b/src/ReferenceCop.MSBuild/BuildEngineExtensions.cs @@ -27,6 +27,16 @@ public static void LogViolation(this IBuildEngine self, Violation violation, str } } + public static void LogTraceMessage(this IBuildEngine self, string message) + { + var messageEvent = new BuildMessageEventArgs( + message: $"[TRACE]: {message}", + helpKeyword: default, + senderName: SenderName, + importance: MessageImportance.Normal); + self.LogMessageEvent(messageEvent); + } + public static void LogDebugMessage(this IBuildEngine self, string message) { var warningEvent = new BuildWarningEventArgs( diff --git a/src/ReferenceCop.MSBuild/ReferenceCopTask.cs b/src/ReferenceCop.MSBuild/ReferenceCopTask.cs index d370247..a240dd3 100644 --- a/src/ReferenceCop.MSBuild/ReferenceCopTask.cs +++ b/src/ReferenceCop.MSBuild/ReferenceCopTask.cs @@ -13,15 +13,16 @@ public class ReferenceCopTask : ITask private readonly IProjectMetadataProvider projectReferencesProvider; private readonly Func configLoaderFactory; - private readonly Func> projectTagViolationDetectorFactory; - private readonly Func> projectPathViolationDetectorFactory; + private readonly Func> projectTagViolationDetectorFactory; + private readonly Func> projectPathViolationDetectorFactory; + private readonly Func traceWriterFactory; /// /// Initializes a new instance of the class. /// The constructor for the ReferenceCopTask used by MSBuild. /// public ReferenceCopTask() - : this(new MSBuildProjectMetadataProvider(), null, null, null) + : this(new MSBuildProjectMetadataProvider(), null, null, null, null) { } @@ -38,16 +39,40 @@ public ReferenceCopTask( IConfigurationLoader configLoader, IViolationDetector tagViolationDetector, IViolationDetector pathViolationDetector) + : this(projectReferencesProvider, configLoader, tagViolationDetector, pathViolationDetector, null) + { + } + + /// + /// Initializes a new instance of the class. + /// Full constructor supporting dependency injection of all components. + /// + public ReferenceCopTask( + IProjectMetadataProvider projectReferencesProvider, + IConfigurationLoader configLoader, + IViolationDetector tagViolationDetector, + IViolationDetector pathViolationDetector, + Func traceWriterFactory) { this.projectReferencesProvider = projectReferencesProvider; this.configLoaderFactory = (configFilePaths) => configLoader ?? new XmlConfigurationLoader(configFilePaths); - this.projectTagViolationDetectorFactory = (config, projectPath) => - tagViolationDetector ?? new ProjectTagViolationDetector(config, projectPath, new ProjectTagProvider()); + this.projectTagViolationDetectorFactory = (config, projectPath, tw) => + tagViolationDetector ?? new ProjectTagViolationDetector(config, projectPath, new ProjectTagProvider(), tw); + + this.projectPathViolationDetectorFactory = (config, projectPath, repositoryRoot, tw) => + pathViolationDetector ?? new ProjectPathViolationDetector(config, projectPath, new ProjectPathProvider(repositoryRoot), tw); + + this.traceWriterFactory = traceWriterFactory ?? (enableTracing => + { + if (enableTracing) + { + return new TraceWriter(); + } - this.projectPathViolationDetectorFactory = (config, projectPath, repositoryRoot) => - pathViolationDetector ?? new ProjectPathViolationDetector(config, projectPath, new ProjectPathProvider(repositoryRoot)); + return NullTraceWriter.Instance; + }); } public IBuildEngine BuildEngine { get; set; } @@ -73,17 +98,24 @@ public bool Execute() var configLoader = this.configLoaderFactory(configFilePath); var config = configLoader.Load(); + var traceWriter = this.traceWriterFactory(config.EnableTracing); + if (config.EnableDebugMessages && config.UseExperimentalDetectors) { this.BuildEngine.LogDebugMessage("Using experimental detectors"); } + if (config.EnableTracing) + { + this.BuildEngine.LogDebugMessage("Tracing is enabled"); + } + 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); + var projectTagViolationDetector = this.projectTagViolationDetectorFactory(config, this.ProjectFile.ItemSpec, traceWriter); var projectTagViolations = config.UseExperimentalDetectors ? projectTagViolationDetector.GetViolationsFromExperimental(evaluationContexts) : projectTagViolationDetector.GetViolationsFrom(evaluationContexts); @@ -100,7 +132,7 @@ public bool Execute() var repositoryRoot = this.projectReferencesProvider.GetPropertyValue( this.ProjectFile.ItemSpec, ReferenceCopRepositoryRootProperty); - var projectPathViolationDetector = this.projectPathViolationDetectorFactory(config, this.ProjectFile.ItemSpec, repositoryRoot); + var projectPathViolationDetector = this.projectPathViolationDetectorFactory(config, this.ProjectFile.ItemSpec, repositoryRoot, traceWriter); var projectPathViolations = config.UseExperimentalDetectors ? projectPathViolationDetector.GetViolationsFromExperimental(evaluationContexts) : projectPathViolationDetector.GetViolationsFrom(evaluationContexts); @@ -114,6 +146,15 @@ public bool Execute() this.BuildEngine.LogViolation(violation, this.ProjectFile.ItemSpec); } + + // Flush trace messages to the build log + if (traceWriter is TraceWriter tw) + { + foreach (var message in tw.Messages) + { + this.BuildEngine.LogTraceMessage(message); + } + } } catch (Exception ex) { diff --git a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs index 81fb862..ab21b06 100644 --- a/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs +++ b/src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs @@ -42,7 +42,11 @@ public override void Initialize(AnalysisContext context) { var configLoader = new XmlConfigurationLoader(compilationAnalysisContext); this.config = configLoader.Load(); - this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), this.config); + var traceWriter = this.config.EnableTracing + ? (ITraceWriter)new TraceWriter() + : NullTraceWriter.Instance; + + this.assemblyNameViolationDetector = new AssemblyNameViolationDetector(new PatternMatchComparer(), this.config, traceWriter); if (this.config.EnableDebugMessages && this.config.UseExperimentalDetectors) { @@ -51,6 +55,16 @@ public override void Initialize(AnalysisContext context) } this.AnalyzeCompilation(compilationAnalysisContext); + + // Flush trace messages as diagnostics + if (traceWriter is TraceWriter tw) + { + foreach (var message in tw.Messages) + { + compilationAnalysisContext.ReportDiagnostic( + DiagnosticFactory.CreateDebugMessage($"[TRACE]: {message}")); + } + } } catch (Exception ex) { diff --git a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs index 6bea93c..d1a07f2 100644 --- a/src/ReferenceCop/Configuration/ReferenceCopConfig.cs +++ b/src/ReferenceCop/Configuration/ReferenceCopConfig.cs @@ -13,6 +13,7 @@ public ReferenceCopConfig() this.Rules = new List(); this.UseExperimentalDetectors = false; this.EnableDebugMessages = false; + this.EnableTracing = false; } [XmlElement] @@ -21,6 +22,9 @@ public ReferenceCopConfig() [XmlElement] public bool EnableDebugMessages { get; set; } + [XmlElement] + public bool EnableTracing { 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 009508e..54164c9 100644 --- a/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs +++ b/src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs @@ -11,11 +11,18 @@ public class AssemblyNameViolationDetector : IViolationDetector exactMatchRules; private readonly List> patternRules; private readonly IEqualityComparer referenceNameComparer; + private readonly ITraceWriter traceWriter; public AssemblyNameViolationDetector(IEqualityComparer referenceNameComparer, ReferenceCopConfig config) + : this(referenceNameComparer, config, NullTraceWriter.Instance) + { + } + + public AssemblyNameViolationDetector(IEqualityComparer referenceNameComparer, ReferenceCopConfig config, ITraceWriter traceWriter) { this.rules = new Dictionary(referenceNameComparer); this.referenceNameComparer = referenceNameComparer; + this.traceWriter = traceWriter ?? NullTraceWriter.Instance; // Separate exact matches from patterns for performance optimization. this.exactMatchRules = new Dictionary(StringComparer.InvariantCulture); @@ -26,6 +33,11 @@ public AssemblyNameViolationDetector(IEqualityComparer referenceNameComp public IEnumerable GetViolationsFrom(IEnumerable> references) { + if (this.traceWriter.IsEnabled) + { + this.traceWriter.Write($"[AssemblyNameViolationDetector] Evaluating references against {this.rules.Count} rule(s)"); + } + foreach (var rule in this.rules) { foreach (var referenceContext in references) @@ -41,9 +53,19 @@ public IEnumerable GetViolationsFrom(IEnumerable GetViolationsFromExperimental(IEnumerable GetViolationsFromExperimental(IEnumerable private readonly string projectFilePath; private readonly IProjectPathProvider projectPathProvider; + private readonly ITraceWriter traceWriter; public ProjectPathViolationDetector(ReferenceCopConfig config, string projectFilePath, IProjectPathProvider projectPathProvider) + : this(config, projectFilePath, projectPathProvider, NullTraceWriter.Instance) + { + } + + public ProjectPathViolationDetector(ReferenceCopConfig config, string projectFilePath, IProjectPathProvider projectPathProvider, ITraceWriter traceWriter) { this.LoadRulesFrom(config); this.projectFilePath = projectFilePath; this.projectPathProvider = projectPathProvider; + this.traceWriter = traceWriter ?? NullTraceWriter.Instance; } public IEnumerable GetViolationsFrom(IEnumerable> references) { var fromProjectPath = this.projectPathProvider.GetRelativePath(this.projectFilePath); + if (this.traceWriter.IsEnabled) + { + this.traceWriter.Write($"[ProjectPathViolationDetector] Evaluating project '{this.projectFilePath}' with relative path '{fromProjectPath}'"); + this.traceWriter.Write($"[ProjectPathViolationDetector] Loaded {this.rules.Count} rule(s)"); + } + foreach (var rule in this.rules) { if (fromProjectPath.StartsWith(rule.FromPath)) { + if (this.traceWriter.IsEnabled) + { + this.traceWriter.Write($"[ProjectPathViolationDetector] Rule '{rule.Name}': FromPath '{rule.FromPath}' matches project path '{fromProjectPath}'"); + } + foreach (var referenceContext in references) { var toProjectPath = this.projectPathProvider.GetRelativePath(referenceContext.Reference); @@ -34,13 +52,31 @@ public IEnumerable GetViolationsFrom(IEnumerable private readonly string projectFilePath; private readonly IProjectTagProvider projectTagProvider; + private readonly ITraceWriter traceWriter; public ProjectTagViolationDetector(ReferenceCopConfig config, string projectFilePath, IProjectTagProvider projectTagProvider) + : this(config, projectFilePath, projectTagProvider, NullTraceWriter.Instance) + { + } + + public ProjectTagViolationDetector(ReferenceCopConfig config, string projectFilePath, IProjectTagProvider projectTagProvider, ITraceWriter traceWriter) { this.LoadRulesFrom(config); this.projectFilePath = projectFilePath; this.projectTagProvider = projectTagProvider; + this.traceWriter = traceWriter ?? NullTraceWriter.Instance; } public IEnumerable GetViolationsFrom(IEnumerable> references) { var fromProjectTag = this.projectTagProvider.GetProjectTag(this.projectFilePath); + if (this.traceWriter.IsEnabled) + { + this.traceWriter.Write($"[ProjectTagViolationDetector] Evaluating project '{this.projectFilePath}' with tag '{fromProjectTag}'"); + this.traceWriter.Write($"[ProjectTagViolationDetector] Loaded {this.rules.Count} rule(s)"); + } + foreach (var rule in this.rules) { if (fromProjectTag == rule.FromProjectTag) { + if (this.traceWriter.IsEnabled) + { + this.traceWriter.Write($"[ProjectTagViolationDetector] Rule '{rule.Name}' matched FromProjectTag '{rule.FromProjectTag}'"); + } + foreach (var referenceContext in references) { var toProjectTag = this.projectTagProvider.GetProjectTag(referenceContext.Reference); @@ -34,13 +52,31 @@ public IEnumerable GetViolationsFrom(IEnumerable + /// Interface for writing trace messages during rule evaluation. + /// + public interface ITraceWriter + { + /// + /// Writes a trace message. + /// + /// The trace message. + void Write(string message); + + /// + /// Gets a value indicating whether tracing is enabled. + /// + bool IsEnabled { get; } + } +} diff --git a/src/ReferenceCop/Tracing/NullTraceWriter.cs b/src/ReferenceCop/Tracing/NullTraceWriter.cs new file mode 100644 index 0000000..d470124 --- /dev/null +++ b/src/ReferenceCop/Tracing/NullTraceWriter.cs @@ -0,0 +1,17 @@ +namespace ReferenceCop +{ + /// + /// A no-op trace writer that discards all messages. Used when tracing is disabled. + /// + public class NullTraceWriter : ITraceWriter + { + public static readonly NullTraceWriter Instance = new NullTraceWriter(); + + public bool IsEnabled => false; + + public void Write(string message) + { + // Intentionally empty. + } + } +} diff --git a/src/ReferenceCop/Tracing/TraceWriter.cs b/src/ReferenceCop/Tracing/TraceWriter.cs new file mode 100644 index 0000000..db75507 --- /dev/null +++ b/src/ReferenceCop/Tracing/TraceWriter.cs @@ -0,0 +1,40 @@ +namespace ReferenceCop +{ + using System; + using System.Collections.Generic; + + /// + /// A trace writer that collects trace messages in memory. + /// Messages can be retrieved via the property or + /// forwarded to an external handler via the callback. + /// + public class TraceWriter : ITraceWriter + { + private readonly List messages = new List(); + + /// + /// Gets a value indicating whether tracing is enabled. + /// + public bool IsEnabled => true; + + /// + /// Gets the collected trace messages. + /// + public IReadOnlyList Messages => this.messages; + + /// + /// Gets or sets an optional callback invoked for each trace message. + /// + public Action OnMessage { get; set; } + + /// + /// Writes a trace message. + /// + /// The trace message. + public void Write(string message) + { + this.messages.Add(message); + this.OnMessage?.Invoke(message); + } + } +}