|
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Tools.Formatters;
using Microsoft.Extensions.Logging;
namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal class AnalyzerFormatter : ICodeFormatter
{
public static AnalyzerFormatter CodeStyleFormatter => new AnalyzerFormatter(
Resources.Code_Style,
FixCategory.CodeStyle,
includeCompilerDiagnostics: false,
new CodeStyleInformationProvider(),
new AnalyzerRunner(),
new SolutionCodeFixApplier());
public static AnalyzerFormatter ThirdPartyFormatter => new AnalyzerFormatter(
Resources.Analyzer_Reference,
FixCategory.Analyzers,
includeCompilerDiagnostics: true,
new AnalyzerReferenceInformationProvider(),
new AnalyzerRunner(),
new SolutionCodeFixApplier());
private readonly string _name;
private readonly bool _includeCompilerDiagnostics;
private readonly IAnalyzerInformationProvider _informationProvider;
private readonly IAnalyzerRunner _runner;
private readonly ICodeFixApplier _applier;
public FixCategory Category { get; }
public AnalyzerFormatter(
string name,
FixCategory category,
bool includeCompilerDiagnostics,
IAnalyzerInformationProvider informationProvider,
IAnalyzerRunner runner,
ICodeFixApplier applier)
{
_name = name;
Category = category;
_includeCompilerDiagnostics = includeCompilerDiagnostics;
_informationProvider = informationProvider;
_runner = runner;
_applier = applier;
}
public async Task<Solution> FormatAsync(
Workspace workspace,
Solution solution,
ImmutableArray<DocumentId> formattableDocuments,
FormatOptions formatOptions,
ILogger logger,
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken)
{
var projectAnalyzersAndFixers = _informationProvider.GetAnalyzersAndFixers(workspace, solution, formatOptions, logger);
if (projectAnalyzersAndFixers.IsEmpty)
{
return solution;
}
var allFixers = projectAnalyzersAndFixers.Values.SelectMany(analyzersAndFixers => analyzersAndFixers.Fixers).ToImmutableArray();
// Only include compiler diagnostics if we have an associated fixer that supports FixAllScope.Solution
var fixableCompilerDiagnostics = _includeCompilerDiagnostics
? allFixers
.Where(codefix => codefix.GetFixAllProvider()?.GetSupportedFixAllScopes()?.Contains(FixAllScope.Solution) == true)
.SelectMany(codefix => codefix.FixableDiagnosticIds.Where(id => id.StartsWith("CS") || id.StartsWith("BC")))
.ToImmutableHashSet()
: ImmutableHashSet<string>.Empty;
// Filter compiler diagnostics
if (!fixableCompilerDiagnostics.IsEmpty && !formatOptions.Diagnostics.IsEmpty)
{
fixableCompilerDiagnostics.Intersect(formatOptions.Diagnostics);
}
var analysisStopwatch = Stopwatch.StartNew();
logger.LogTrace(Resources.Running_0_analysis, _name);
var formattablePaths = await GetFormattablePathsAsync(solution, formattableDocuments, cancellationToken).ConfigureAwait(false);
logger.LogTrace(Resources.Determining_diagnostics);
var severity = _informationProvider.GetSeverity(formatOptions);
// Filter to analyzers that report diagnostics with equal or greater severity.
var projectAnalyzers = await FilterAnalyzersAsync(solution, projectAnalyzersAndFixers, formattablePaths, severity, formatOptions.Diagnostics, formatOptions.ExcludeDiagnostics, cancellationToken).ConfigureAwait(false);
// Determine which diagnostics are being reported for each project.
var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, formatOptions, severity, fixableCompilerDiagnostics, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
var projectDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds;
logger.LogTrace(Resources.Complete_in_0_ms, projectDiagnosticsMS);
// Only run code fixes when we are saving changes.
if (formatOptions.SaveFormattedFiles)
{
logger.LogTrace(Resources.Fixing_diagnostics);
// Run each analyzer individually and apply fixes if possible.
solution = await FixDiagnosticsAsync(solution, projectAnalyzers, allFixers, projectDiagnostics, formattablePaths, formatOptions, severity, fixableCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);
var fixDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds - projectDiagnosticsMS;
logger.LogTrace(Resources.Complete_in_0_ms, fixDiagnosticsMS);
}
logger.LogTrace(Resources.Analysis_complete_in_0ms_, analysisStopwatch.ElapsedMilliseconds);
return solution;
async static Task<ImmutableHashSet<string>> GetFormattablePathsAsync(Solution solution, ImmutableArray<DocumentId> formattableDocuments, CancellationToken cancellationToken)
{
var formattablePaths = ImmutableHashSet.CreateBuilder<string>();
foreach (var documentId in formattableDocuments)
{
var document = solution.GetDocument(documentId);
if (document is null)
{
document = await solution.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false);
if (document is null)
{
continue;
}
}
formattablePaths.Add(document.FilePath!);
}
return formattablePaths.ToImmutable();
}
}
private async Task<ImmutableDictionary<ProjectId, ImmutableHashSet<string>>> GetProjectDiagnosticsAsync(
Solution solution,
ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableHashSet<string> formattablePaths,
FormatOptions options,
DiagnosticSeverity severity,
ImmutableHashSet<string> fixableCompilerDiagnostics,
ILogger logger,
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken)
{
var result = new CodeAnalysisResult(options.Diagnostics, options.ExcludeDiagnostics);
var projects = options.WorkspaceType == WorkspaceType.Solution
? solution.Projects
: solution.Projects.Where(project => project.FilePath == options.WorkspaceFilePath);
foreach (var project in projects)
{
var analyzers = projectAnalyzers[project.Id];
if (analyzers.IsEmpty)
{
continue;
}
// Run all the filtered analyzers to determine which are reporting diagnostic.
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, severity, fixableCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);
}
LogDiagnosticLocations(solution, result.Diagnostics.SelectMany(kvp => kvp.Value), options.SaveFormattedFiles, options.ChangesAreErrors, logger, options.LogLevel, formattedFiles);
return result.Diagnostics.ToImmutableDictionary(kvp => kvp.Key.Id, kvp => kvp.Value.Select(diagnostic => diagnostic.Id).ToImmutableHashSet());
static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> diagnostics, bool saveFormattedFiles, bool changesAreErrors, ILogger logger, LogLevel logLevel, List<FormattedFile> formattedFiles)
{
foreach (var diagnostic in diagnostics)
{
var document = solution.GetDocument(diagnostic.Location.SourceTree);
if (document is null)
{
continue;
}
var mappedLineSpan = diagnostic.Location.GetMappedLineSpan();
var diagnosticPosition = mappedLineSpan.StartLinePosition;
if (!saveFormattedFiles || logLevel == LogLevel.Debug)
{
logger.LogDiagnosticIssue(document, diagnosticPosition, diagnostic, changesAreErrors);
}
formattedFiles.Add(new FormattedFile(document, new[] { new FileChange(diagnosticPosition, diagnostic.Id, $"{diagnostic.Severity.ToString().ToLower()} {diagnostic.Id}: {diagnostic.GetMessage()}") }));
}
}
}
private async Task<Solution> FixDiagnosticsAsync(
Solution solution,
ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableArray<CodeFixProvider> allFixers,
ImmutableDictionary<ProjectId, ImmutableHashSet<string>> projectDiagnostics,
ImmutableHashSet<string> formattablePaths,
FormatOptions options,
DiagnosticSeverity severity,
ImmutableHashSet<string> fixableCompilerDiagnostics,
ILogger logger,
CancellationToken cancellationToken)
{
// Determine the reported diagnostic ids
var reportedDiagnostics = projectDiagnostics.SelectMany(kvp => kvp.Value).Distinct().ToImmutableArray();
if (reportedDiagnostics.IsEmpty)
{
return solution;
}
var fixersById = CreateFixerMap(reportedDiagnostics, allFixers);
// We need to run each codefix iteratively so ensure that all diagnostics are found and fixed.
foreach (var diagnosticId in reportedDiagnostics)
{
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(options.Diagnostics, options.ExcludeDiagnostics);
foreach (var project in solution.Projects)
{
// Only run analysis on projects that had previously reported the diagnostic
if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds)
|| !diagnosticIds.Contains(diagnosticId))
{
continue;
}
var analyzers = projectAnalyzers[project.Id]
.Where(analyzer => analyzer.SupportedDiagnostics.Any(descriptor => descriptor.Id == diagnosticId))
.ToImmutableArray();
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, severity, fixableCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);
}
var hasDiagnostics = result.Diagnostics.Any(kvp => kvp.Value.Count > 0);
if (hasDiagnostics)
{
foreach (var codefix in codefixes)
{
var changedSolution = await _applier.ApplyCodeFixesAsync(solution, result, codefix, diagnosticId, logger, cancellationToken).ConfigureAwait(false);
if (changedSolution.GetChanges(solution).Any())
{
solution = changedSolution;
}
}
}
}
return solution;
static ImmutableDictionary<string, ImmutableArray<CodeFixProvider>> CreateFixerMap(
ImmutableArray<string> diagnosticIds,
ImmutableArray<CodeFixProvider> fixers)
{
return diagnosticIds.ToImmutableDictionary(
id => id,
id => fixers
.Where(fixer => ContainsFixableId(fixer, id))
.ToImmutableArray());
}
static bool ContainsFixableId(CodeFixProvider fixer, string id)
{
// The unnecessary imports diagnostic and fixer use a special diagnostic id.
if (id == "IDE0005" && fixer.FixableDiagnosticIds.Contains("RemoveUnnecessaryImportsFixable"))
{
return true;
}
return fixer.FixableDiagnosticIds.Contains(id);
}
}
internal static async Task<ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>>> FilterAnalyzersAsync(
Solution solution,
ImmutableDictionary<ProjectId, AnalyzersAndFixers> projectAnalyzersAndFixers,
ImmutableHashSet<string> formattablePaths,
DiagnosticSeverity minimumSeverity,
ImmutableHashSet<string> diagnostics,
ImmutableHashSet<string> excludeDiagnostics,
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<ProjectId, ImmutableArray<DiagnosticAnalyzer>>();
foreach (var projectId in projectAnalyzersAndFixers.Keys)
{
var project = solution.GetProject(projectId);
if (project is null)
{
continue;
}
// Skip if the project does not contain any of the formattable paths.
if (!project.Documents.Any(d => d.FilePath is not null && formattablePaths.Contains(d.FilePath)))
{
projectAnalyzers.Add(projectId, ImmutableArray<DiagnosticAnalyzer>.Empty);
continue;
}
var analyzers = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
// Filter analyzers by project's language
var filteredAnalyzer = projectAnalyzersAndFixers[projectId].Analyzers
.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, project.Language));
foreach (var analyzer in filteredAnalyzer)
{
// Allow suppressors unconditionally
if (analyzer is DiagnosticSuppressor suppressor)
{
analyzers.Add(suppressor);
continue;
}
// Filter by excluded diagnostics
if (!excludeDiagnostics.IsEmpty &&
analyzer.SupportedDiagnostics.All(descriptor => excludeDiagnostics.Contains(descriptor.Id)))
{
continue;
}
// Filter by diagnostics
if (!diagnostics.IsEmpty &&
!analyzer.SupportedDiagnostics.Any(descriptor => diagnostics.Contains(descriptor.Id)))
{
continue;
}
// 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") == true)
{
analyzers.Add(analyzer);
continue;
}
var severity = await analyzer.GetSeverityAsync(project, formattablePaths, cancellationToken).ConfigureAwait(false);
if (severity >= minimumSeverity)
{
analyzers.Add(analyzer);
}
}
projectAnalyzers.Add(projectId, analyzers.ToImmutableArray());
}
return projectAnalyzers.ToImmutableDictionary();
}
private static bool DoesAnalyzerSupportLanguage(DiagnosticAnalyzer analyzer, string language)
{
return analyzer.GetType()
.GetCustomAttributes(typeof(DiagnosticAnalyzerAttribute), true)
.OfType<DiagnosticAnalyzerAttribute>()
.Any(attribute => attribute.Languages.Contains(language));
}
}
}
|