New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Run all analyzers even when a fixer isn't present #723
Changes from all commits
57e8aa5
42d4fee
c4c3ca0
c986271
0db01ca
e89105c
38fdc7d
efd4767
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,18 +17,18 @@ namespace Microsoft.CodeAnalysis.Tools.Analyzers | |
internal class AnalyzerFormatter : ICodeFormatter | ||
{ | ||
private readonly string _name; | ||
private readonly IAnalyzerFinder _finder; | ||
private readonly IAnalyzerInformationProvider _informationProvider; | ||
private readonly IAnalyzerRunner _runner; | ||
private readonly ICodeFixApplier _applier; | ||
|
||
public AnalyzerFormatter( | ||
string name, | ||
IAnalyzerFinder finder, | ||
IAnalyzerInformationProvider informationProvider, | ||
IAnalyzerRunner runner, | ||
ICodeFixApplier applier) | ||
{ | ||
_name = name; | ||
_finder = finder; | ||
_informationProvider = informationProvider; | ||
_runner = runner; | ||
_applier = applier; | ||
} | ||
|
@@ -41,11 +41,7 @@ internal class AnalyzerFormatter : ICodeFormatter | |
List<FormattedFile> formattedFiles, | ||
CancellationToken cancellationToken) | ||
{ | ||
var analyzersAndFixers = _finder.GetAnalyzersAndFixers(solution, formatOptions, logger); | ||
if (analyzersAndFixers.Length == 0) | ||
{ | ||
return solution; | ||
} | ||
var (analyzers, fixers) = _informationProvider.GetAnalyzersAndFixers(solution, formatOptions, logger); | ||
|
||
var analysisStopwatch = Stopwatch.StartNew(); | ||
logger.LogTrace(Resources.Running_0_analysis, _name); | ||
|
@@ -55,17 +51,21 @@ internal class AnalyzerFormatter : ICodeFormatter | |
|
||
logger.LogTrace(Resources.Determining_diagnostics); | ||
|
||
var allAnalyzers = analyzersAndFixers.Select(pair => pair.Analyzer).ToImmutableArray(); | ||
var projectAnalyzers = await _finder.FilterBySeverityAsync(solution.Projects, allAnalyzers, formattablePaths, formatOptions, cancellationToken).ConfigureAwait(false); | ||
var severity = _informationProvider.GetSeverity(formatOptions); | ||
|
||
var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, formatOptions, logger, formattedFiles, cancellationToken).ConfigureAwait(false); | ||
// Filter to analyzers that report diagnostics with equal or greater severity. | ||
var projectAnalyzers = await FilterBySeverityAsync(solution.Projects, analyzers, formattablePaths, severity, cancellationToken).ConfigureAwait(false); | ||
|
||
// Determine which diagnostics are being reported for each project. | ||
var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, formatOptions, severity, logger, formattedFiles, cancellationToken).ConfigureAwait(false); | ||
|
||
var projectDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds; | ||
logger.LogTrace(Resources.Complete_in_0_ms, projectDiagnosticsMS); | ||
|
||
logger.LogTrace(Resources.Fixing_diagnostics); | ||
|
||
solution = await FixDiagnosticsAsync(solution, analyzersAndFixers, projectDiagnostics, formattablePaths, logger, cancellationToken).ConfigureAwait(false); | ||
// Run each analyzer individually and apply fixes if possible. | ||
solution = await FixDiagnosticsAsync(solution, analyzers, fixers, projectDiagnostics, formattablePaths, severity, logger, cancellationToken).ConfigureAwait(false); | ||
|
||
var fixDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds - projectDiagnosticsMS; | ||
logger.LogTrace(Resources.Complete_in_0_ms, fixDiagnosticsMS); | ||
|
@@ -80,6 +80,7 @@ internal class AnalyzerFormatter : ICodeFormatter | |
ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers, | ||
ImmutableHashSet<string> formattablePaths, | ||
FormatOptions options, | ||
DiagnosticSeverity severity, | ||
ILogger logger, | ||
List<FormattedFile> formattedFiles, | ||
CancellationToken cancellationToken) | ||
|
@@ -93,7 +94,8 @@ internal class AnalyzerFormatter : ICodeFormatter | |
continue; | ||
} | ||
|
||
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, logger, cancellationToken).ConfigureAwait(false); | ||
// Run all the filtered analyzers to determine which are reporting diagnostic. | ||
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, severity, logger, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
LogDiagnosticLocations(solution, result.Diagnostics.SelectMany(kvp => kvp.Value), options.WorkspaceFilePath, options.ChangesAreErrors, logger, formattedFiles); | ||
|
@@ -106,7 +108,7 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di | |
|
||
foreach (var diagnostic in diagnostics) | ||
{ | ||
var message = diagnostic.GetMessage(); | ||
var message = $"{diagnostic.GetMessage()} ({diagnostic.Id})"; | ||
var filePath = diagnostic.Location.SourceTree?.FilePath; | ||
var document = solution.GetDocument(diagnostic.Location.SourceTree); | ||
|
||
|
@@ -130,40 +132,121 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di | |
|
||
private async Task<Solution> FixDiagnosticsAsync( | ||
Solution solution, | ||
ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers, | ||
ImmutableArray<DiagnosticAnalyzer> allAnalyzers, | ||
ImmutableArray<CodeFixProvider> allCodefixes, | ||
ImmutableDictionary<ProjectId, ImmutableHashSet<string>> projectDiagnostics, | ||
ImmutableHashSet<string> formattablePaths, | ||
DiagnosticSeverity severity, | ||
ILogger logger, | ||
CancellationToken cancellationToken) | ||
{ | ||
// we need to run each codefix iteratively so ensure that all diagnostics are found and fixed | ||
foreach (var (analyzer, codefix) in analyzersAndFixers) | ||
// Determine the reported diagnostic ids | ||
var reportedDiagnostics = projectDiagnostics.SelectMany(kvp => kvp.Value).Distinct().ToImmutableArray(); | ||
if (reportedDiagnostics.IsEmpty) | ||
{ | ||
return solution; | ||
} | ||
|
||
// Build maps between diagnostic id and the associated analyzers and codefixes | ||
var analyzersById = CreateAnalyzerMap(reportedDiagnostics, allAnalyzers); | ||
var fixersById = CreateFixerMap(reportedDiagnostics, allCodefixes); | ||
|
||
// We need to run each codefix iteratively so ensure that all diagnostics are found and fixed. | ||
foreach (var diagnosticId in reportedDiagnostics) | ||
{ | ||
var analyzers = analyzersById[diagnosticId]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps we should try to get the value from the dictionary? Isn't it expected that we would not find entries for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, the |
||
var codefixes = fixersById[diagnosticId]; | ||
|
||
// If there is no codefix, there is no reason to run analysis again. | ||
if (codefixes.IsEmpty) | ||
{ | ||
logger.LogWarning(Resources.Unable_to_fix_0_No_associated_code_fix_found, diagnosticId); | ||
continue; | ||
} | ||
|
||
var result = new CodeAnalysisResult(); | ||
foreach (var project in solution.Projects) | ||
{ | ||
if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds) || | ||
!analyzer.SupportedDiagnostics.Any(diagnostic => diagnosticIds.Contains(diagnostic.Id))) | ||
// Only run analysis on projects that had previously reported the diagnostic | ||
if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds)) | ||
{ | ||
continue; | ||
} | ||
|
||
await _runner.RunCodeAnalysisAsync(result, analyzer, project, formattablePaths, logger, cancellationToken).ConfigureAwait(false); | ||
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, severity, logger, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
var hasDiagnostics = result.Diagnostics.Any(kvp => kvp.Value.Count > 0); | ||
if (hasDiagnostics && codefix != null) | ||
if (hasDiagnostics) | ||
{ | ||
solution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken).ConfigureAwait(false); | ||
var changedSolution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken).ConfigureAwait(false); | ||
if (changedSolution.GetChanges(solution).Any()) | ||
foreach (var codefix in codefixes) | ||
{ | ||
solution = changedSolution; | ||
var changedSolution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, logger, cancellationToken).ConfigureAwait(false); | ||
if (changedSolution.GetChanges(solution).Any()) | ||
{ | ||
solution = changedSolution; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
return solution; | ||
|
||
static ImmutableDictionary<string, ImmutableArray<DiagnosticAnalyzer>> CreateAnalyzerMap( | ||
ImmutableArray<string> diagnosticIds, | ||
ImmutableArray<DiagnosticAnalyzer> analyzers) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like we use an inconsistent wrapping style. Which one do we prefer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this one because it doesn't change when things are renamed/refactored. |
||
{ | ||
return diagnosticIds.ToImmutableDictionary( | ||
id => id, | ||
id => analyzers.Where(analyzer => analyzer.SupportedDiagnostics.Any(diagnostic => diagnostic.Id == id)).ToImmutableArray()); | ||
} | ||
|
||
static ImmutableDictionary<string, ImmutableArray<CodeFixProvider>> CreateFixerMap( | ||
ImmutableArray<string> diagnosticIds, | ||
ImmutableArray<CodeFixProvider> fixers) | ||
{ | ||
return diagnosticIds.ToImmutableDictionary( | ||
id => id, | ||
id => fixers.Where(fixer => fixer.FixableDiagnosticIds.Contains(id)).ToImmutableArray()); | ||
} | ||
} | ||
|
||
internal static async Task<ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>>> FilterBySeverityAsync( | ||
IEnumerable<Project> projects, | ||
ImmutableArray<DiagnosticAnalyzer> allAnalyzers, | ||
ImmutableHashSet<string> formattablePaths, | ||
DiagnosticSeverity minimumSeverity, | ||
CancellationToken cancellationToken) | ||
{ | ||
// We only want to run analyzers for each project that have the potential for reporting a diagnostic with | ||
// a severity equal to or greater than specified. | ||
var projectAnalyzers = ImmutableDictionary.CreateBuilder<Project, ImmutableArray<DiagnosticAnalyzer>>(); | ||
foreach (var project in projects) | ||
{ | ||
var analyzers = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>(); | ||
|
||
foreach (var analyzer in allAnalyzers) | ||
{ | ||
// Always run naming style analyzers because we cannot determine potential severity. | ||
// The reported diagnostics will be filtered by severity when they are run. | ||
if (analyzer.GetType().FullName.EndsWith("NamingStyleDiagnosticAnalyzer")) | ||
{ | ||
analyzers.Add(analyzer); | ||
continue; | ||
} | ||
|
||
var severity = await analyzer.GetSeverityAsync(project, formattablePaths, cancellationToken).ConfigureAwait(false); | ||
if (severity >= minimumSeverity) | ||
{ | ||
analyzers.Add(analyzer); | ||
} | ||
} | ||
|
||
projectAnalyzers.Add(project, analyzers.ToImmutableArray()); | ||
} | ||
|
||
return projectAnalyzers.ToImmutableDictionary(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,4 @@ | ||
// 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. | ||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Linq; | ||
|
@@ -44,14 +42,17 @@ private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string | |
return false; | ||
} | ||
|
||
// If user has explicitly configured severity for this diagnostic ID, that should be respected and | ||
// bulk configuration should not be applied. | ||
// If user has explicitly configured severity for this diagnostic ID, that should be respected. | ||
if (compilation.Options.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out severity)) | ||
{ | ||
return true; | ||
} | ||
|
||
// If user has explicitly configured severity for this diagnostic ID, that should be respected. | ||
// For example, 'dotnet_diagnostic.CA1000.severity = error' | ||
if (compilation.Options.SpecificDiagnosticOptions.ContainsKey(descriptor.Id) || | ||
tree.DiagnosticOptions.ContainsKey(descriptor.Id)) | ||
if (tree.DiagnosticOptions.TryGetValue(descriptor.Id, out severity)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were not returning the severity from DiagnosticOptions causing some analyzers to not be run. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this is why we need a test for this :( |
||
{ | ||
severity = default; | ||
return false; | ||
return true; | ||
} | ||
|
||
var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some of the goodness from Roslyn's .editorconfig