From 128de98f1c9dd38856a6d22814f92f28708c3ad8 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sun, 7 Jun 2020 11:05:24 -0700 Subject: [PATCH] Filter analyzers by severity before adding to compilation --- src/Analyzers/AnalyzerFormatter.cs | 48 +++++- src/Analyzers/AnalyzerOptionExtensions.cs | 118 ++++++++++++++ src/Analyzers/AnalyzerRunner.cs | 7 +- src/Analyzers/Extensions.cs | 166 +++++++++++++++++++- src/Analyzers/Interfaces/IAnalyzerRunner.cs | 4 +- 5 files changed, 329 insertions(+), 14 deletions(-) create mode 100644 src/Analyzers/AnalyzerOptionExtensions.cs diff --git a/src/Analyzers/AnalyzerFormatter.cs b/src/Analyzers/AnalyzerFormatter.cs index b71bdbb046..90034b3b11 100644 --- a/src/Analyzers/AnalyzerFormatter.cs +++ b/src/Analyzers/AnalyzerFormatter.cs @@ -45,11 +45,14 @@ internal class AnalyzerFormatter : ICodeFormatter var analyzersAndFixers = _finder.GetAnalyzersAndFixers(); var formattablePaths = formattableDocuments.Select(id => solution.GetDocument(id)?.FilePath) - .OfType().ToImmutableArray(); + .OfType().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); @@ -67,22 +70,53 @@ internal class AnalyzerFormatter : ICodeFormatter logger.LogTrace("Analysis complete in {0}ms.", analysisStopwatch.ElapsedMilliseconds); return solution; + + static async Task>> FilterBySeverityAsync( + IEnumerable projects, + ImmutableArray allAnalyzers, + ImmutableHashSet formattablePaths, + DiagnosticSeverity minimumSeverity, + CancellationToken cancellationToken) + { + var projectAnalyzers = ImmutableDictionary.CreateBuilder>(); + foreach (var project in projects) + { + var analyzers = ImmutableArray.CreateBuilder(); + + 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>> GetProjectDiagnosticsAsync( Solution solution, - ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers, - ImmutableArray formattablePaths, + ImmutableDictionary> projectAnalyzers, + ImmutableHashSet formattablePaths, FormatOptions options, ILogger logger, List 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); } @@ -120,7 +154,7 @@ static void LogDiagnosticLocations(IEnumerable diagnostics, string w Solution solution, ImmutableArray<(DiagnosticAnalyzer Analyzer, CodeFixProvider? Fixer)> analyzersAndFixers, ImmutableDictionary> projectDiagnostics, - ImmutableArray formattablePaths, + ImmutableHashSet formattablePaths, ILogger logger, CancellationToken cancellationToken) { diff --git a/src/Analyzers/AnalyzerOptionExtensions.cs b/src/Analyzers/AnalyzerOptionExtensions.cs new file mode 100644 index 0000000000..157a558e5a --- /dev/null +++ b/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}"; + + /// + /// Tries to get configured severity for the given + /// for the given from bulk configuration analyzer config options, i.e. + /// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%' + /// or + /// 'dotnet_analyzer_diagnostic.severity = %severity%' + /// + 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; + } + } +} diff --git a/src/Analyzers/AnalyzerRunner.cs b/src/Analyzers/AnalyzerRunner.cs index c0a799cfea..21d8f239a3 100644 --- a/src/Analyzers/AnalyzerRunner.cs +++ b/src/Analyzers/AnalyzerRunner.cs @@ -16,7 +16,7 @@ internal partial class AnalyzerRunner : IAnalyzerRunner CodeAnalysisResult result, DiagnosticAnalyzer analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken) => RunCodeAnalysisAsync(result, ImmutableArray.Create(analyzers), project, formattableDocumentPaths, logger, cancellationToken); @@ -25,7 +25,7 @@ internal partial class AnalyzerRunner : IAnalyzerRunner CodeAnalysisResult result, ImmutableArray analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken) { @@ -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); } diff --git a/src/Analyzers/Extensions.cs b/src/Analyzers/Extensions.cs index b7a0337a66..d56e7b5c6e 100644 --- a/src/Analyzers/Extensions.cs +++ b/src/Analyzers/Extensions.cs @@ -1,22 +1,43 @@ // 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.Collections; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.Tools.Analyzers { public static class Extensions { + private static Assembly MicrosoftCodeAnalysisFeaturesAssembly { get; } + private static Type IDEDiagnosticIdToOptionMappingHelperType { get; } + private static MethodInfo TryGetMappedOptionsMethod { get; } + + static Extensions() + { + MicrosoftCodeAnalysisFeaturesAssembly = Assembly.Load(new AssemblyName("Microsoft.CodeAnalysis.Features")); + IDEDiagnosticIdToOptionMappingHelperType = MicrosoftCodeAnalysisFeaturesAssembly.GetType("Microsoft.CodeAnalysis.Diagnostics.IDEDiagnosticIdToOptionMappingHelper"); + TryGetMappedOptionsMethod = IDEDiagnosticIdToOptionMappingHelperType.GetMethod("TryGetMappedOptions", BindingFlags.Static | BindingFlags.Public); + } + public static bool Any(this SolutionChanges solutionChanges) - => solutionChanges.GetProjectChanges().Any(x => x.GetChangedDocuments().Any()); + => solutionChanges.GetProjectChanges().Any(x => x.GetChangedDocuments().Any()); public static bool TryCreateInstance(this Type type, [NotNullWhen(returnValue: true)] out T? instance) where T : class { try { - var defaultCtor = type.GetConstructor(new Type[] { }); + var defaultCtor = type.GetConstructor( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + binder: null, + new Type[] { }, + modifiers: null); instance = defaultCtor != null ? (T)Activator.CreateInstance( @@ -34,5 +55,146 @@ public static bool Any(this SolutionChanges solutionChanges) throw new InvalidOperationException($"Failed to create instrance of {type.FullName} in {type.AssemblyQualifiedName}.", ex); } } + + /// + /// Get the highest possible severity for any formattable document in the project. + /// + public static async Task GetSeverityAsync( + this DiagnosticAnalyzer analyzer, + Project project, + ImmutableHashSet formattablePaths, + CancellationToken cancellationToken) + { + var severity = DiagnosticSeverity.Hidden; + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return severity; + } + + foreach (var document in project.Documents) + { + // Is the document formattable? + if (document.FilePath is null || !formattablePaths.Contains(document.FilePath)) + { + continue; + } + + var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + + var documentSeverity = analyzer.GetSeverity(document, project.AnalyzerOptions, options, compilation); + if (documentSeverity > severity) + { + severity = documentSeverity; + } + } + + return severity; + } + + private static DiagnosticSeverity GetSeverity( + this DiagnosticAnalyzer analyzer, + Document document, + AnalyzerOptions? analyzerOptions, + OptionSet options, + Compilation compilation) + { + var severity = DiagnosticSeverity.Hidden; + + if (!document.TryGetSyntaxTree(out var tree)) + { + return severity; + } + + foreach (var descriptor in analyzer.SupportedDiagnostics) + { + if (severity == DiagnosticSeverity.Error) + { + break; + } + + if (analyzerOptions.TryGetSeverityFromBulkConfiguration(tree, compilation, descriptor, out var reportDiagnostic)) + { + var configuredSeverity = ToSeverity(reportDiagnostic); + if (configuredSeverity > severity) + { + severity = configuredSeverity; + } + continue; + } + + if (TryGetSeverityFromCodeStyleOption(descriptor, compilation, options, out var codeStyleSeverity)) + { + if (codeStyleSeverity > severity) + { + severity = codeStyleSeverity; + } + continue; + } + + if (descriptor.DefaultSeverity > severity) + { + severity = descriptor.DefaultSeverity; + } + } + + return severity; + + static DiagnosticSeverity ToSeverity(ReportDiagnostic reportDiagnostic) + { + return reportDiagnostic switch + { + ReportDiagnostic.Error => DiagnosticSeverity.Error, + ReportDiagnostic.Warn => DiagnosticSeverity.Warning, + ReportDiagnostic.Info => DiagnosticSeverity.Info, + _ => DiagnosticSeverity.Hidden + }; + } + + static bool TryGetSeverityFromCodeStyleOption( + DiagnosticDescriptor descriptor, + Compilation compilation, + OptionSet options, + out DiagnosticSeverity severity) + { + severity = DiagnosticSeverity.Hidden; + + var parameters = new object?[] { descriptor.Id, compilation.Language, null }; + var result = (bool)TryGetMappedOptionsMethod.Invoke(null, parameters); + + if (!result) + { + return false; + } + + var codeStyleOptions = (IEnumerable)parameters[2]!; + foreach (var codeStyleOptionObj in codeStyleOptions) + { + IOption codeStyleOption = (IOption)codeStyleOptionObj; + var option = options.GetOption(new OptionKey(codeStyleOption, codeStyleOption.IsPerLanguage ? compilation.Language : null)); + if (option is null) + { + continue; + } + + var notificationProperty = option.GetType().GetProperty("Notification"); + if (notificationProperty is null) + { + continue; + } + + var notification = notificationProperty.GetValue(option); + var reportDiagnostic = (ReportDiagnostic)notification.GetType().GetProperty("Severity").GetValue(notification); + var codeStyleSeverity = ToSeverity(reportDiagnostic); + + if (codeStyleSeverity > severity) + { + severity = codeStyleSeverity; + } + } + + return true; + } + } } } diff --git a/src/Analyzers/Interfaces/IAnalyzerRunner.cs b/src/Analyzers/Interfaces/IAnalyzerRunner.cs index 68cf23c402..2d264ac134 100644 --- a/src/Analyzers/Interfaces/IAnalyzerRunner.cs +++ b/src/Analyzers/Interfaces/IAnalyzerRunner.cs @@ -14,7 +14,7 @@ interface IAnalyzerRunner CodeAnalysisResult result, DiagnosticAnalyzer analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken); @@ -22,7 +22,7 @@ interface IAnalyzerRunner CodeAnalysisResult result, ImmutableArray analyzers, Project project, - ImmutableArray formattableDocumentPaths, + ImmutableHashSet formattableDocumentPaths, ILogger logger, CancellationToken cancellationToken); }