Skip to content

Commit

Permalink
Merge pull request #723 from JoeRobich/run-all-analyzers
Browse files Browse the repository at this point in the history
Run all analyzers even when a fixer isn't present
  • Loading branch information
JoeRobich committed Jul 6, 2020
2 parents bc6d17b + efd4767 commit ddcce23
Show file tree
Hide file tree
Showing 46 changed files with 347 additions and 269 deletions.
33 changes: 33 additions & 0 deletions .editorconfig
Expand Up @@ -131,6 +131,7 @@ csharp_indent_switch_labels = true
csharp_indent_labels = flush_left

# Prefer "var" everywhere
dotnet_diagnostic.IDE0007.severity = error
csharp_style_var_for_built_in_types = true:error
csharp_style_var_when_type_is_apparent = true:error
csharp_style_var_elsewhere = true:error
Expand Down Expand Up @@ -178,3 +179,35 @@ csharp_space_between_parentheses = false
csharp_prefer_braces = true:silent
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true

# IDE0073: File header
dotnet_diagnostic.IDE0073.severity = warning
file_header_template = Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

# IDE0035: Remove unreachable code
dotnet_diagnostic.IDE0035.severity = warning

# IDE0036: Order modifiers
dotnet_diagnostic.IDE0036.severity = warning

# IDE0040: Add accessibility modifiers
dotnet_diagnostic.IDE0040.severity = warning

# IDE0043: Format string contains invalid placeholder
dotnet_diagnostic.IDE0043.severity = warning

# IDE0044: Make field readonly
dotnet_diagnostic.IDE0044.severity = warning

# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings?
# IDE0051: Remove unused private member
dotnet_diagnostic.IDE0051.severity = warning

# IDE0052: Remove unread private member
dotnet_diagnostic.IDE0052.severity = warning

# IDE0059: Unnecessary assignment to a value
dotnet_diagnostic.IDE0059.severity = warning

# IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = warning
11 changes: 6 additions & 5 deletions .vscode/launch.json
Expand Up @@ -41,15 +41,16 @@
"stopAtEntry": false
},
{
"name": "format format.sln --fix-style --check",
"name": "format format.sln --fix-style warn --check",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/publish/dotnet-format.dll",
"program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/dotnet-format.dll",
"args": [
"format.sln",
"--fix-style",
"warn",
"-v",
"diag",
"--check"
Expand All @@ -63,9 +64,9 @@
"name": "format format.sln --fix-analyzers warn --check",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/publish/dotnet-format.dll",
"program": "${workspaceFolder}/artifacts/bin/dotnet-format/Debug/netcoreapp2.1/dotnet-format.dll",
"args": [
"format.sln",
"--fix-analyzers",
Expand Down
47 changes: 2 additions & 45 deletions src/Analyzers/AnalyzerFinderHelpers.cs
Expand Up @@ -4,8 +4,6 @@
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
Expand All @@ -14,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal static class AnalyzerFinderHelpers
{
public static ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> LoadAnalyzersAndFixers(IEnumerable<Assembly> assemblies)
public static (ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<CodeFixProvider> Fixers) LoadAnalyzersAndFixers(IEnumerable<Assembly> assemblies)
{
var types = assemblies
.SelectMany(assembly => assembly.GetTypes()
Expand All @@ -34,48 +32,7 @@ public static ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixe
.OfType<DiagnosticAnalyzer>()
.ToImmutableArray();

var builder = ImmutableArray.CreateBuilder<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)>();
foreach (var diagnosticAnalyzer in diagnosticAnalyzers)
{
var diagnosticIds = diagnosticAnalyzer.SupportedDiagnostics.Select(diagnostic => diagnostic.Id).ToImmutableHashSet();
var codeFixProvider = codeFixProviders.FirstOrDefault(codeFixProvider => codeFixProvider.FixableDiagnosticIds.Any(id => diagnosticIds.Contains(id)));

if (codeFixProvider is null)
{
continue;
}

builder.Add((diagnosticAnalyzer, codeFixProvider));
}

return builder.ToImmutableArray();
}

public 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();
return (diagnosticAnalyzers, codeFixProviders);
}
}
}
133 changes: 108 additions & 25 deletions src/Analyzers/AnalyzerFormatter.cs
Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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];
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)
{
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();
}
}
}
19 changes: 10 additions & 9 deletions src/Analyzers/AnalyzerOptionExtensions.cs
@@ -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;
Expand Down Expand Up @@ -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))
{
severity = default;
return false;
return true;
}

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

0 comments on commit ddcce23

Please sign in to comment.