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); + } + } +}