Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ReferenceCopConfig.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<xs:sequence>
<xs:element name="UseExperimentalDetectors" type="xs:boolean" minOccurs="0" />
<xs:element name="EnableDebugMessages" type="xs:boolean" minOccurs="0" />
<xs:element name="EnableTracing" type="xs:boolean" minOccurs="0" />
<xs:element name="Rules">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
Expand Down
10 changes: 10 additions & 0 deletions src/ReferenceCop.MSBuild/BuildEngineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
59 changes: 50 additions & 9 deletions src/ReferenceCop.MSBuild/ReferenceCopTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ public class ReferenceCopTask : ITask

private readonly IProjectMetadataProvider projectReferencesProvider;
private readonly Func<string, IConfigurationLoader> configLoaderFactory;
private readonly Func<ReferenceCopConfig, string, IViolationDetector<string>> projectTagViolationDetectorFactory;
private readonly Func<ReferenceCopConfig, string, string, IViolationDetector<string>> projectPathViolationDetectorFactory;
private readonly Func<ReferenceCopConfig, string, ITraceWriter, IViolationDetector<string>> projectTagViolationDetectorFactory;
private readonly Func<ReferenceCopConfig, string, string, ITraceWriter, IViolationDetector<string>> projectPathViolationDetectorFactory;
private readonly Func<bool, ITraceWriter> traceWriterFactory;

/// <summary>
/// Initializes a new instance of the <see cref="ReferenceCopTask"/> class.
/// The constructor for the ReferenceCopTask used by MSBuild.
/// </summary>
public ReferenceCopTask()
: this(new MSBuildProjectMetadataProvider(), null, null, null)
: this(new MSBuildProjectMetadataProvider(), null, null, null, null)
{
}

Expand All @@ -38,16 +39,40 @@ public ReferenceCopTask(
IConfigurationLoader configLoader,
IViolationDetector<string> tagViolationDetector,
IViolationDetector<string> pathViolationDetector)
: this(projectReferencesProvider, configLoader, tagViolationDetector, pathViolationDetector, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ReferenceCopTask"/> class.
/// Full constructor supporting dependency injection of all components.
/// </summary>
public ReferenceCopTask(
IProjectMetadataProvider projectReferencesProvider,
IConfigurationLoader configLoader,
IViolationDetector<string> tagViolationDetector,
IViolationDetector<string> pathViolationDetector,
Func<bool, ITraceWriter> 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; }
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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)
{
Expand Down
16 changes: 15 additions & 1 deletion src/ReferenceCop.Roslyn/ReferenceCopAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand Down
4 changes: 4 additions & 0 deletions src/ReferenceCop/Configuration/ReferenceCopConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public ReferenceCopConfig()
this.Rules = new List<Rule>();
this.UseExperimentalDetectors = false;
this.EnableDebugMessages = false;
this.EnableTracing = false;
}

[XmlElement]
Expand All @@ -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))]
Expand Down
37 changes: 37 additions & 0 deletions src/ReferenceCop/Detectors/AssemblyNameViolationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ public class AssemblyNameViolationDetector : IViolationDetector<AssemblyIdentity
private readonly Dictionary<string, ReferenceCopConfig.Rule> exactMatchRules;
private readonly List<KeyValuePair<string, ReferenceCopConfig.Rule>> patternRules;
private readonly IEqualityComparer<string> referenceNameComparer;
private readonly ITraceWriter traceWriter;

public AssemblyNameViolationDetector(IEqualityComparer<string> referenceNameComparer, ReferenceCopConfig config)
: this(referenceNameComparer, config, NullTraceWriter.Instance)
{
}

public AssemblyNameViolationDetector(IEqualityComparer<string> referenceNameComparer, ReferenceCopConfig config, ITraceWriter traceWriter)
{
this.rules = new Dictionary<string, ReferenceCopConfig.Rule>(referenceNameComparer);
this.referenceNameComparer = referenceNameComparer;
this.traceWriter = traceWriter ?? NullTraceWriter.Instance;

// Separate exact matches from patterns for performance optimization.
this.exactMatchRules = new Dictionary<string, ReferenceCopConfig.Rule>(StringComparer.InvariantCulture);
Expand All @@ -26,6 +33,11 @@ public AssemblyNameViolationDetector(IEqualityComparer<string> referenceNameComp

public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationContext<AssemblyIdentity>> 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)
Expand All @@ -41,9 +53,19 @@ public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationC
// Check if this warning should be suppressed
if (referenceContext.IsWarningSuppressed)
{
if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[AssemblyNameViolationDetector] Rule '{rule.Value.Name}': violation suppressed for '{reference.Name}'");
}

continue;
}

if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[AssemblyNameViolationDetector] Rule '{rule.Value.Name}': VIOLATION for '{reference.Name}' (pattern='{rule.Key}')");
}

yield return new Violation(rule.Value, reference.Name);
}
}
Expand Down Expand Up @@ -72,12 +94,22 @@ public IEnumerable<Violation> GetViolationsFromExperimental(IEnumerable<Referenc
// Skip if warning is suppressed
if (referenceContext.IsWarningSuppressed)
{
if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[AssemblyNameViolationDetector] Reference '{reference.Name}': warning suppressed, skipping");
}

continue;
}

// Check exact match rules with O(1) lookup
if (this.exactMatchRules.TryGetValue(reference.Name, out var exactRule))
{
if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[AssemblyNameViolationDetector] Rule '{exactRule.Name}': VIOLATION for '{reference.Name}' (exact match)");
}

yield return new Violation(exactRule, reference.Name);
}

Expand All @@ -86,6 +118,11 @@ public IEnumerable<Violation> GetViolationsFromExperimental(IEnumerable<Referenc
{
if (this.referenceNameComparer.Equals(patternRule.Key, reference.Name))
{
if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[AssemblyNameViolationDetector] Rule '{patternRule.Value.Name}': VIOLATION for '{reference.Name}' (pattern='{patternRule.Key}')");
}

yield return new Violation(patternRule.Value, reference.Name);
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/ReferenceCop/Detectors/ProjectPathViolationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,40 @@ public class ProjectPathViolationDetector : IViolationDetector<string>

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<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationContext<string>> 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);
Expand All @@ -34,13 +52,31 @@ public IEnumerable<Violation> GetViolationsFrom(IEnumerable<ReferenceEvaluationC
// Check if this warning should be suppressed
if (referenceContext.IsWarningSuppressed)
{
if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[ProjectPathViolationDetector] Rule '{rule.Name}': violation suppressed for reference '{referenceContext.Reference}'");
}

continue;
}

if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[ProjectPathViolationDetector] Rule '{rule.Name}': VIOLATION for reference '{referenceContext.Reference}' (ToPath='{toProjectPath}')");
}

yield return new Violation(rule, referenceContext.Reference);
}
else if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[ProjectPathViolationDetector] Rule '{rule.Name}': reference path '{toProjectPath}' does not start with '{rule.ToPath}', no match");
}
}
}
else if (this.traceWriter.IsEnabled)
{
this.traceWriter.Write($"[ProjectPathViolationDetector] Rule '{rule.Name}': FromPath '{rule.FromPath}' does not match project path '{fromProjectPath}', skipping");
}
}
}

Expand Down
Loading
Loading