Skip to content

Commit

Permalink
Filter analyzers by severity before adding to compilation
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeRobich committed Jun 7, 2020
1 parent 7b8edc8 commit 128de98
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 14 deletions.
48 changes: 41 additions & 7 deletions src/Analyzers/AnalyzerFormatter.cs
Expand Up @@ -45,11 +45,14 @@ internal class AnalyzerFormatter : ICodeFormatter

var analyzersAndFixers = _finder.GetAnalyzersAndFixers();
var formattablePaths = formattableDocuments.Select(id => solution.GetDocument(id)?.FilePath)
.OfType<string>().ToImmutableArray();
.OfType<string>().ToImmutableHashSet();

logger.LogTrace("Determining diagnostics.");

var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, analyzersAndFixers, formattablePaths, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
var allAnalyzers = analyzersAndFixers.Select(pair => pair.Analyzer).ToImmutableArray();
var projectAnalyzers = await FilterBySeverityAsync(solution.Projects, allAnalyzers, formattablePaths, DiagnosticSeverity.Warning, cancellationToken).ConfigureAwait(false);

var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, options, logger, formattedFiles, cancellationToken).ConfigureAwait(false);

var projectDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds;
logger.LogTrace(Resources.Complete_in_0_ms, projectDiagnosticsMS);
Expand All @@ -67,22 +70,53 @@ internal class AnalyzerFormatter : ICodeFormatter
logger.LogTrace("Analysis complete in {0}ms.", analysisStopwatch.ElapsedMilliseconds);

return solution;

static async Task<ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>>> FilterBySeverityAsync(
IEnumerable<Project> projects,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
ImmutableHashSet<string> formattablePaths,
DiagnosticSeverity minimumSeverity,
CancellationToken cancellationToken)
{
var projectAnalyzers = ImmutableDictionary.CreateBuilder<Project, ImmutableArray<DiagnosticAnalyzer>>();
foreach (var project in projects)
{
var analyzers = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();

foreach (var analyzer in allAnalyzers)
{
var severity = await analyzer.GetSeverityAsync(project, formattablePaths, cancellationToken).ConfigureAwait(false);
if (severity >= minimumSeverity)
{
analyzers.Add(analyzer);
}
}

projectAnalyzers.Add(project, analyzers.ToImmutableArray());
}

return projectAnalyzers.ToImmutableDictionary();
}
}

private async Task<ImmutableDictionary<ProjectId, ImmutableHashSet<string>>> GetProjectDiagnosticsAsync(
Solution solution,
ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers,
ImmutableArray<string> formattablePaths,
ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableHashSet<string> formattablePaths,
FormatOptions options,
ILogger logger,
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken)
{
var analyzers = analyzersAndFixers.Select(pair => pair.Analyzer).ToImmutableArray();

var result = new CodeAnalysisResult();
foreach (var project in solution.Projects)
{
var analyzers = projectAnalyzers[project];
if (analyzers.Length == 0)
{
continue;
}

await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, logger, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -120,7 +154,7 @@ static void LogDiagnosticLocations(IEnumerable<Diagnostic> diagnostics, string w
Solution solution,
ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers,
ImmutableDictionary<ProjectId, ImmutableHashSet<string>> projectDiagnostics,
ImmutableArray<string> formattablePaths,
ImmutableHashSet<string> formattablePaths,
ILogger logger,
CancellationToken cancellationToken)
{
Expand Down
118 changes: 118 additions & 0 deletions src/Analyzers/AnalyzerOptionExtensions.cs
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Linq;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal static class AnalyzerOptionsExtensions
{
private const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic";
private const string CategoryPrefix = "category";
private const string SeveritySuffix = "severity";

private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix;

private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category)
=> $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}";

/// <summary>
/// Tries to get configured severity for the given <paramref name="descriptor"/>
/// for the given <paramref name="tree"/> from bulk configuration analyzer config options, i.e.
/// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%'
/// or
/// 'dotnet_analyzer_diagnostic.severity = %severity%'
/// </summary>
public static bool TryGetSeverityFromBulkConfiguration(
this AnalyzerOptions? analyzerOptions,
SyntaxTree tree,
Compilation compilation,
DiagnosticDescriptor descriptor,
out ReportDiagnostic severity)
{
// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (analyzerOptions == null ||
!descriptor.IsEnabledByDefault ||
descriptor.CustomTags.Any(tag => tag == WellKnownDiagnosticTags.Compiler || tag == WellKnownDiagnosticTags.NotConfigurable))
{
severity = default;
return false;
}

// If user has explicitly configured severity for this diagnostic ID, that should be respected and
// bulk configuration should not be applied.
// For example, 'dotnet_diagnostic.CA1000.severity = error'
if (compilation.Options.SpecificDiagnosticOptions.ContainsKey(descriptor.Id) ||
tree.DiagnosticOptions.ContainsKey(descriptor.Id))
{
severity = default;
return false;
}

var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree);

// If user has explicitly configured default severity for the diagnostic category, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
var categoryBasedKey = GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(descriptor.Category);
if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out var value) &&
TryParseSeverity(value, out severity))
{
return true;
}

// Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.severity = error'
if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
TryParseSeverity(value, out severity))
{
return true;
}

severity = default;
return false;
}

internal static bool TryParseSeverity(string value, out ReportDiagnostic severity)
{
var comparer = StringComparer.OrdinalIgnoreCase;
if (comparer.Equals(value, "default"))
{
severity = ReportDiagnostic.Default;
return true;
}
else if (comparer.Equals(value, "error"))
{
severity = ReportDiagnostic.Error;
return true;
}
else if (comparer.Equals(value, "warning"))
{
severity = ReportDiagnostic.Warn;
return true;
}
else if (comparer.Equals(value, "suggestion"))
{
severity = ReportDiagnostic.Info;
return true;
}
else if (comparer.Equals(value, "silent") || comparer.Equals(value, "refactoring"))
{
severity = ReportDiagnostic.Hidden;
return true;
}
else if (comparer.Equals(value, "none"))
{
severity = ReportDiagnostic.Suppress;
return true;
}

severity = default;
return false;
}
}
}
7 changes: 4 additions & 3 deletions src/Analyzers/AnalyzerRunner.cs
Expand Up @@ -16,7 +16,7 @@ internal partial class AnalyzerRunner : IAnalyzerRunner
CodeAnalysisResult result,
DiagnosticAnalyzer analyzers,
Project project,
ImmutableArray<string> formattableDocumentPaths,
ImmutableHashSet<string> formattableDocumentPaths,
ILogger logger,
CancellationToken cancellationToken)
=> RunCodeAnalysisAsync(result, ImmutableArray.Create(analyzers), project, formattableDocumentPaths, logger, cancellationToken);
Expand All @@ -25,7 +25,7 @@ internal partial class AnalyzerRunner : IAnalyzerRunner
CodeAnalysisResult result,
ImmutableArray<DiagnosticAnalyzer> analyzers,
Project project,
ImmutableArray<string> formattableDocumentPaths,
ImmutableHashSet<string> formattableDocumentPaths,
ILogger logger,
CancellationToken cancellationToken)
{
Expand All @@ -50,7 +50,8 @@ internal partial class AnalyzerRunner : IAnalyzerRunner
if (!diagnostic.IsSuppressed &&
diagnostic.Severity >= DiagnosticSeverity.Warning &&
diagnostic.Location.IsInSource &&
formattableDocumentPaths.Contains(diagnostic.Location.SourceTree?.FilePath, StringComparer.OrdinalIgnoreCase))
diagnostic.Location.SourceTree is object &&
formattableDocumentPaths.Contains(diagnostic.Location.SourceTree.FilePath))
{
result.AddDiagnostic(project, diagnostic);
}
Expand Down

0 comments on commit 128de98

Please sign in to comment.