From e5f9839bddd5bc3ffc848ab79cf3d3562919c2eb Mon Sep 17 00:00:00 2001 From: Manish Vasani Date: Mon, 24 Feb 2020 14:21:20 -0800 Subject: [PATCH] Add lightbulb actions to bulk configure default severity for analyzer diagnostics Fixes #41910 --- .../AllAnalyzersSeverityConfigurationTests.cs | 341 ++++++++++++++++++ ...CategoryBasedSeverityConfigurationTests.cs | 339 +++++++++++++++++ .../Suggestions/SuggestedActionsSource.cs | 48 ++- ...actConfigurationActionWithNestedActions.cs | 9 + .../Configuration/ConfigurationUpdater.cs | 192 +++++++--- ...TopLevelBulkConfigureSeverityCodeAction.cs | 33 ++ .../ConfigureSeverityLevelCodeFixProvider.cs | 47 ++- .../Core/Portable/FeaturesResources.resx | 6 + .../Portable/xlf/FeaturesResources.cs.xlf | 10 + .../Portable/xlf/FeaturesResources.de.xlf | 10 + .../Portable/xlf/FeaturesResources.es.xlf | 10 + .../Portable/xlf/FeaturesResources.fr.xlf | 10 + .../Portable/xlf/FeaturesResources.it.xlf | 10 + .../Portable/xlf/FeaturesResources.ja.xlf | 10 + .../Portable/xlf/FeaturesResources.ko.xlf | 10 + .../Portable/xlf/FeaturesResources.pl.xlf | 10 + .../Portable/xlf/FeaturesResources.pt-BR.xlf | 10 + .../Portable/xlf/FeaturesResources.ru.xlf | 10 + .../Portable/xlf/FeaturesResources.tr.xlf | 10 + .../xlf/FeaturesResources.zh-Hans.xlf | 10 + .../xlf/FeaturesResources.zh-Hant.xlf | 10 + .../CSharp/CSharpCodeActions.cs | 12 + .../Core/Portable/CodeFixes/CodeFix.cs | 2 +- 23 files changed, 1098 insertions(+), 61 deletions(-) create mode 100644 src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/AllAnalyzersSeverityConfigurationTests.cs create mode 100644 src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/CategoryBasedSeverityConfigurationTests.cs create mode 100644 src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/AllAnalyzersSeverityConfigurationTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/AllAnalyzersSeverityConfigurationTests.cs new file mode 100644 index 00000000000..649e16d2c4e --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/AllAnalyzersSeverityConfigurationTests.cs @@ -0,0 +1,341 @@ +// 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.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.Configuration.ConfigureSeverity +{ + public abstract partial class AllAnalyzersSeverityConfigurationTests : AbstractSuppressionDiagnosticTest + { + private sealed class CustomDiagnosticAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + id: "XYZ0001", + title: "Title", + messageFormat: "Message", + category: "CustomCategory", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction( + c => c.ReportDiagnostic(Diagnostic.Create(Rule, c.Node.GetLocation())), + SyntaxKind.ClassDeclaration); + } + } + + protected override TestWorkspace CreateWorkspaceFromFile(string initialMarkup, TestParameters parameters) + => TestWorkspace.CreateCSharp(initialMarkup, parameters.parseOptions, parameters.compilationOptions); + + protected override string GetLanguage() => LanguageNames.CSharp; + + protected override ParseOptions GetScriptOptions() => Options.Script; + + internal override Tuple CreateDiagnosticProviderAndFixer(Workspace workspace) + { + return new Tuple( + new CustomDiagnosticAnalyzer(), new ConfigureSeverityLevelCodeFixProvider()); + } + + public sealed class SilentConfigurationTests : AllAnalyzersSeverityConfigurationTests + { + /// + /// Code action ranges: + /// 1. (0 - 4) => Code actions for diagnostic "ID" configuration with severity None, Silent, Suggestion, Warning and Error + /// 2. (5 - 9) => Code actions for diagnostic "Category" configuration with severity None, Silent, Suggestion, Warning and Error + /// 3. (10 - 14) => Code actions for all analyzer diagnostics configuration with severity None, Silent, Suggestion, Warning and Error + /// + protected override int CodeActionIndex => 11; + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_Empty() + { + var input = @" + + + +[|class Program1 { }|] + + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RuleExists() + { + var input = @" + + + +[|class Program1 { }|] + + [*.cs] +dotnet_analyzer_diagnostic.severity = suggestion # Comment + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] +dotnet_analyzer_diagnostic.severity = silent # Comment + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RuleIdEntryExists() + { + var input = @" + + + +[|class Program1 { }|] + + [*.cs] +dotnet_diagnostic.XYZ0001.severity = suggestion # Comment1 +dotnet_diagnostic.category-CustomCategory.severity = warning # Comment2 + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] +dotnet_diagnostic.XYZ0001.severity = suggestion # Comment1 +dotnet_diagnostic.category-CustomCategory.severity = warning # Comment2 + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_InvalidHeader() + { + var input = @" + + + +[|class Program1 { }|] + + [*.vb] +dotnet_analyzer_diagnostic.severity = suggestion + + +"; + + var expected = @" + + + +class Program1 { } + + [*.vb] +dotnet_analyzer_diagnostic.severity = suggestion + +[*.cs] + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_MaintainExistingEntry() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, input, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_DiagnosticsSuppressed() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.severity = none + + +"; + + await TestMissingInRegularAndScriptAsync(input); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_InvalidRule() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.XYZ1111.severity = suggestion + + +"; + + var expected = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.XYZ1111.severity = suggestion + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RegexHeaderMatch() + { + // NOTE: Even though we have a regex match, bulk configuration code fix is always applied to all files + // within the editorconfig cone, so it generates a new entry. + var input = @" + + + +[|class Program1 { }|] + + [*am/fi*e.cs] +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = warning + + +"; + + var expected = @" + + + +class Program1 { } + + [*am/fi*e.cs] +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = warning + +[*.cs] + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RegexHeaderNonMatch() + { + var input = @" + + + +[|class Program1 { }|] + + [*am/fii*e.cs] +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = warning + + +"; + + var expected = @" + + + +class Program1 { } + + [*am/fii*e.cs] +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = warning + +[*.cs] + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + } + } +} diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/CategoryBasedSeverityConfigurationTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/CategoryBasedSeverityConfigurationTests.cs new file mode 100644 index 00000000000..278dffbe76f --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Diagnostics/Configuration/ConfigureSeverity/CategoryBasedSeverityConfigurationTests.cs @@ -0,0 +1,339 @@ +// 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.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.Configuration.ConfigureSeverity +{ + public abstract partial class CategoryBasedSeverityConfigurationTests : AbstractSuppressionDiagnosticTest + { + private sealed class CustomDiagnosticAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + id: "XYZ0001", + title: "Title", + messageFormat: "Message", + category: "CustomCategory", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction( + c => c.ReportDiagnostic(Diagnostic.Create(Rule, c.Node.GetLocation())), + SyntaxKind.ClassDeclaration); + } + } + + protected override TestWorkspace CreateWorkspaceFromFile(string initialMarkup, TestParameters parameters) + => TestWorkspace.CreateCSharp(initialMarkup, parameters.parseOptions, parameters.compilationOptions); + + protected override string GetLanguage() => LanguageNames.CSharp; + + protected override ParseOptions GetScriptOptions() => Options.Script; + + internal override Tuple CreateDiagnosticProviderAndFixer(Workspace workspace) + { + return new Tuple( + new CustomDiagnosticAnalyzer(), new ConfigureSeverityLevelCodeFixProvider()); + } + + public sealed class SilentConfigurationTests : CategoryBasedSeverityConfigurationTests + { + /// + /// Code action ranges: + /// 1. (0 - 4) => Code actions for diagnostic "ID" configuration with severity None, Silent, Suggestion, Warning and Error + /// 2. (5 - 9) => Code actions for diagnostic "Category" configuration with severity None, Silent, Suggestion, Warning and Error + /// 3. (10 - 14) => Code actions for all analyzer diagnostics configuration with severity None, Silent, Suggestion, Warning and Error + /// + protected override int CodeActionIndex => 6; + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_Empty() + { + var input = @" + + + +[|class Program1 { }|] + + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RuleExists() + { + var input = @" + + + +[|class Program1 { }|] + + [*.cs] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = suggestion # Comment + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent # Comment + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RuleIdEntryExists() + { + var input = @" + + + +[|class Program1 { }|] + + [*.cs] +dotnet_diagnostic.XYZ0001.severity = suggestion # Comment + + +"; + + var expected = @" + + + +class Program1 { } + + [*.cs] +dotnet_diagnostic.XYZ0001.severity = suggestion # Comment + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_InvalidHeader() + { + var input = @" + + + +[|class Program1 { }|] + + [*.vb] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = suggestion + + +"; + + var expected = @" + + + +class Program1 { } + + [*.vb] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = suggestion + +[*.cs] + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_MaintainExistingEntry() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, input, CodeActionIndex); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_DiagnosticsSuppressed() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.category-CustomCategory.severity = none + + +"; + + await TestMissingInRegularAndScriptAsync(input); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_InvalidRule() + { + var input = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.category-XYZ1111Category.severity = suggestion + + +"; + + var expected = @" + + + +[|class Program1 { }|] + + [*.{vb,cs}] +dotnet_analyzer_diagnostic.category-XYZ1111Category.severity = suggestion + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RegexHeaderMatch() + { + // NOTE: Even though we have a regex match, bulk configuration code fix is always applied to all files + // within the editorconfig cone, so it generates a new entry. + var input = @" + + + +[|class Program1 { }|] + + [*am/fi*e.cs] +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = warning + + +"; + + var expected = @" + + + +class Program1 { } + + [*am/fi*e.cs] +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = warning + +[*.cs] + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + + [ConditionalFact(typeof(IsEnglishLocal)), Trait(Traits.Feature, Traits.Features.CodeActionsConfiguration)] + public async Task ConfigureEditorconfig_RegexHeaderNonMatch() + { + var input = @" + + + +[|class Program1 { }|] + + [*am/fii*e.cs] +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = warning + + +"; + + var expected = @" + + + +class Program1 { } + + [*am/fii*e.cs] +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = warning + +[*.cs] + +# Default severity for analyzer diagnostics with category 'CustomCategory' +dotnet_analyzer_diagnostic.category-CustomCategory.severity = silent + + +"; + + await TestInRegularAndScriptAsync(input, expected, CodeActionIndex); + } + } + } +} diff --git a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs index 4f239629753..4907a659236 100644 --- a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs +++ b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs @@ -29,7 +29,7 @@ using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; -using CodeFixGroupKey = System.Tuple; +using CodeFixGroupKey = System.Tuple; using IUIThreadOperationContext = Microsoft.VisualStudio.Utilities.IUIThreadOperationContext; namespace Microsoft.CodeAnalysis.Editor.Implementation.Suggestions @@ -496,6 +496,9 @@ private ImmutableArray FilterOnUIThread(ImmutableArray action is AbstractConfigurationActionWithNestedActions; + private static bool IsBulkConfigurationAction(CodeAction action) + => (action as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == true; + private void AddCodeActions( Workspace workspace, IDictionary> map, ArrayBuilder order, CodeFixCollection fixCollection, @@ -542,9 +545,7 @@ SuggestedAction GetSuggestedAction(CodeAction action, CodeFix fix) IDictionary> map, ArrayBuilder order) { - var diag = fix.GetPrimaryDiagnosticData(); - - var groupKey = new CodeFixGroupKey(diag, fix.Action.Priority); + var groupKey = GetGroupKey(fix); if (!map.ContainsKey(groupKey)) { order.Add(groupKey); @@ -552,6 +553,17 @@ SuggestedAction GetSuggestedAction(CodeAction action, CodeFix fix) } map[groupKey].Add(suggestedAction); + + static CodeFixGroupKey GetGroupKey(CodeFix fix) + { + var diag = fix.GetPrimaryDiagnosticData(); + if (fix.Action is AbstractConfigurationActionWithNestedActions configurationAction) + { + return new CodeFixGroupKey(diag, configurationAction.Priority, configurationAction.AdditionalPriority); + } + + return new CodeFixGroupKey(diag, fix.Action.Priority, null); + } } /// @@ -612,20 +624,30 @@ SuggestedAction GetSuggestedAction(CodeAction action, CodeFix fix) { using var nonSuppressionSetsDisposer = ArrayBuilder.GetInstance(out var nonSuppressionSets); using var suppressionSetsDisposer = ArrayBuilder.GetInstance(out var suppressionSets); + using var bulkConfigurationActionsDisposer = ArrayBuilder.GetInstance(out var bulkConfigurationActions); - foreach (var diag in order) + foreach (var groupKey in order) { - var actions = map[diag]; + var actions = map[groupKey]; var nonSuppressionActions = actions.Where(a => !IsTopLevelSuppressionAction(a.CodeAction)); - AddSuggestedActionsSet(nonSuppressionActions, diag, nonSuppressionSets); + AddSuggestedActionsSet(nonSuppressionActions, groupKey, nonSuppressionSets); + + var suppressionActions = actions.Where(a => IsTopLevelSuppressionAction(a.CodeAction) && !IsBulkConfigurationAction(a.CodeAction)); + AddSuggestedActionsSet(suppressionActions, groupKey, suppressionSets); - var suppressionActions = actions.Where(a => IsTopLevelSuppressionAction(a.CodeAction)); - AddSuggestedActionsSet(suppressionActions, diag, suppressionSets); + bulkConfigurationActions.AddRange(actions.Where(a => IsBulkConfigurationAction(a.CodeAction))); } var sets = nonSuppressionSets.ToImmutable(); + // Append bulk configuration fixes at the end of suppression/configuration fixes. + if (bulkConfigurationActions.Count > 0) + { + var bulkConfigurationSet = new SuggestedActionSet(PredefinedSuggestedActionCategoryNames.CodeFix, bulkConfigurationActions); + suppressionSets.Add(bulkConfigurationSet); + } + if (suppressionSets.Count > 0) { // Wrap the suppression/configuration actions within another top level suggested action @@ -694,7 +716,7 @@ SuggestedAction GetSuggestedAction(CodeAction action, CodeFix fix) private static void AddSuggestedActionsSet( IEnumerable actions, - CodeFixGroupKey diag, + CodeFixGroupKey groupKey, ArrayBuilder sets) { foreach (var group in actions.GroupBy(a => a.Priority)) @@ -702,9 +724,9 @@ SuggestedAction GetSuggestedAction(CodeAction action, CodeFix fix) var priority = GetSuggestedActionSetPriority(group.Key); // diagnostic from things like build shouldn't reach here since we don't support LB for those diagnostics - Debug.Assert(diag.Item1.HasTextSpan); - var category = GetFixCategory(diag.Item1.Severity); - sets.Add(new SuggestedActionSet(category, group, priority: priority, applicableToSpan: diag.Item1.GetTextSpan().ToSpan())); + Debug.Assert(groupKey.Item1.HasTextSpan); + var category = GetFixCategory(groupKey.Item1.Severity); + sets.Add(new SuggestedActionSet(category, group, priority: priority, applicableToSpan: groupKey.Item1.GetTextSpan().ToSpan())); } } diff --git a/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs b/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs index df25cbd96fa..260514a2302 100644 --- a/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs +++ b/src/Features/Core/Portable/CodeFixes/AbstractConfigurationActionWithNestedActions.cs @@ -21,5 +21,14 @@ protected AbstractConfigurationActionWithNestedActions(ImmutableArray CodeActionPriority.None; + + /// + /// Additional priority associated with all configuration and suppression code actions. + /// This allows special code actions such as Bulk configuration to to be at the end of + /// all suppression and configuration actions by having a lower additional priority. + /// + internal virtual CodeActionPriority AdditionalPriority => CodeActionPriority.Medium; + + internal virtual bool IsBulkConfigurationAction => false; } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs index 98b80391d18..e13bd5a660b 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; @@ -30,11 +31,15 @@ internal sealed partial class ConfigurationUpdater private enum ConfigurationKind { OptionValue, - Severity + Severity, + BulkConfigure } private const string DiagnosticOptionPrefix = "dotnet_diagnostic."; - private const string DiagnosticOptionSuffix = ".severity"; + private const string SeveritySuffix = ".severity"; + private const string BulkConfigureAllAnalyzerDiagnosticsOptionKey = "dotnet_analyzer_diagnostic.severity"; + private const string BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix = "dotnet_analyzer_diagnostic.category-"; + private const string AllAnalyzerDiagnosticsCategory = ""; // Regular expression for .editorconfig header. // For example: "[*.cs] # Optional comment" @@ -47,38 +52,46 @@ private enum ConfigurationKind // For example: "dotnet_style_object_initializer = true:suggestion # Optional comment" private static readonly Regex s_optionBasedEntryPattern = new Regex(@"([\w ]+)=([\w, ]+):[ ]*([\w]+)([ ]*[;#].*)?"); - // Regular expression for .editorconfig diagnosticID severity configuration entry. - // For example: "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" - private static readonly Regex s_severityBasedEntryPattern = new Regex(@"([\w \.]+)=[ ]*([\w]+)([ ]*[;#].*)?"); + // Regular expression for .editorconfig diagnostic severity configuration entry. + // For example: + // 1. "dotnet_diagnostic.CA2000.severity = suggestion # Optional comment" + // 2. "dotnet_analyzer_diagnostic.category-Security.severity = suggestion # Optional comment" + // 3. "dotnet_analyzer_diagnostic.severity = suggestion # Optional comment" + private static readonly Regex s_severityBasedEntryPattern = new Regex(@"([\w- \.]+)=[ ]*([\w]+)([ ]*[;#].*)?"); - private readonly string _optionNameOpt; - private readonly string _newOptionValueOpt; + private readonly string? _optionNameOpt; + private readonly string? _newOptionValueOpt; private readonly string _newSeverity; private readonly ConfigurationKind _configurationKind; - private readonly Diagnostic _diagnostic; + private readonly Diagnostic? _diagnostic; + private readonly string? _categoryToBulkConfigure; private readonly bool _isPerLanguage; private readonly Project _project; private readonly CancellationToken _cancellationToken; private readonly string _language; private ConfigurationUpdater( - string optionNameOpt, - string newOptionValueOpt, + string? optionNameOpt, + string? newOptionValueOpt, string newSeverity, ConfigurationKind configurationKind, - Diagnostic diagnostic, + Diagnostic? diagnosticToConfigure, + string? categoryToBulkConfigure, bool isPerLanguage, Project project, CancellationToken cancellationToken) { Debug.Assert(configurationKind != ConfigurationKind.OptionValue || !string.IsNullOrEmpty(newOptionValueOpt)); Debug.Assert(!string.IsNullOrEmpty(newSeverity)); + Debug.Assert(diagnosticToConfigure != null ^ categoryToBulkConfigure != null); + Debug.Assert((categoryToBulkConfigure != null) == (configurationKind == ConfigurationKind.BulkConfigure)); _optionNameOpt = optionNameOpt; _newOptionValueOpt = newOptionValueOpt; _newSeverity = newSeverity; _configurationKind = configurationKind; - _diagnostic = diagnostic; + _diagnostic = diagnosticToConfigure; + _categoryToBulkConfigure = categoryToBulkConfigure; _isPerLanguage = isPerLanguage; _project = project; _cancellationToken = cancellationToken; @@ -129,11 +142,51 @@ private enum ConfigurationKind else { updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, - configurationKind: ConfigurationKind.Severity, diagnostic, isPerLanguage: false, project, cancellationToken); + configurationKind: ConfigurationKind.Severity, diagnostic, categoryToBulkConfigure: null, isPerLanguage: false, project, cancellationToken); return updater.ConfigureAsync(); } } + /// + /// Updates or adds an .editorconfig to the given + /// so that the default severity of the diagnostics with the given is configured to be the given + /// . + /// + public static Task BulkConfigureSeverityAsync( + string editorConfigSeverity, + string category, + Project project, + CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(!string.IsNullOrEmpty(category)); + return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category, project, cancellationToken); + } + + /// + /// Updates or adds an .editorconfig to the given + /// so that the default severity of all diagnostics is configured to be the given + /// . + /// + public static Task BulkConfigureSeverityAsync( + string editorConfigSeverity, + Project project, + CancellationToken cancellationToken) + { + return BulkConfigureSeverityCoreAsync(editorConfigSeverity, category: AllAnalyzerDiagnosticsCategory, project, cancellationToken); + } + + private static Task BulkConfigureSeverityCoreAsync( + string editorConfigSeverity, + string category, + Project project, + CancellationToken cancellationToken) + { + Contract.ThrowIfNull(category); + var updater = new ConfigurationUpdater(optionNameOpt: null, newOptionValueOpt: null, editorConfigSeverity, + configurationKind: ConfigurationKind.BulkConfigure, diagnosticToConfigure: null, category, isPerLanguage: false, project, cancellationToken); + return updater.ConfigureAsync(); + } + /// /// Updates or adds an .editorconfig to the given /// so that the given is configured to have the given . @@ -164,9 +217,10 @@ private enum ConfigurationKind Debug.Assert(optionValue != null); Debug.Assert(!string.IsNullOrEmpty(severity)); - var updater = new ConfigurationUpdater(optionName, optionValue, severity, configurationKind, diagnostic, isPerLanguage, currentProject, cancellationToken); + var updater = new ConfigurationUpdater(optionName, optionValue, severity, configurationKind, + diagnostic, categoryToBulkConfigure: null, isPerLanguage, currentProject, cancellationToken); var solution = await updater.ConfigureAsync().ConfigureAwait(false); - currentProject = solution.GetProject(project.Id); + currentProject = solution.GetProject(project.Id)!; } return currentProject.Solution; @@ -201,9 +255,11 @@ private async Task ConfigureAsync() return solution.WithAnalyzerConfigDocumentText(editorConfigDocument.Id, newText); } - private AnalyzerConfigDocument FindOrGenerateEditorConfig() + private AnalyzerConfigDocument? FindOrGenerateEditorConfig() { - var analyzerConfigPath = _project.TryGetAnalyzerConfigPathForDiagnosticConfiguration(_diagnostic); + var analyzerConfigPath = _diagnostic != null + ? _project.TryGetAnalyzerConfigPathForDiagnosticConfiguration(_diagnostic) + : _project.TryGetAnalyzerConfigPathForProjectConfiguration(); if (analyzerConfigPath == null) { return null; @@ -217,7 +273,7 @@ private AnalyzerConfigDocument FindOrGenerateEditorConfig() } // Otherwise, add analyzer config document to all applicable projects for the current project's solution. - AnalyzerConfigDocument analyzerConfigDocument = null; + AnalyzerConfigDocument? analyzerConfigDocument = null; var analyzerConfigDirectory = PathUtilities.GetDirectoryName(analyzerConfigPath); var currentSolution = _project.Solution; foreach (var projectId in _project.Solution.ProjectIds) @@ -338,7 +394,7 @@ private AnalyzerConfigDocument FindOrGenerateEditorConfig() return ImmutableArray<(OptionKey, ICodeStyleOption, IEditorConfigStorageLocation2, bool)>.Empty; } - private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, AnalyzerConfigDocument editorConfigDocument) + private SourceText? GetNewAnalyzerConfigDocumentText(SourceText originalText, AnalyzerConfigDocument editorConfigDocument) { // Check if an entry to configure the rule severity already exists in the .editorconfig file. // If it does, we update the existing entry with the new severity. @@ -353,7 +409,7 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana return AddMissingRule(originalText, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); } - private (SourceText newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) CheckIfRuleExistsAndReplaceInFile( + private (SourceText? newText, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) CheckIfRuleExistsAndReplaceInFile( SourceText result, AnalyzerConfigDocument editorConfigDocument) { @@ -367,8 +423,8 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana var relativePath = string.Empty; var diagnosticFilePath = string.Empty; - // If diagnostic SourceTree is null, it means Location.None, and thus no relative path. - var diagnosticSourceTree = _diagnostic.Location.SourceTree; + // If diagnostic SourceTree is null, it means either Location.None or Bulk configuration at root editorconfig, and thus no relative path. + var diagnosticSourceTree = _diagnostic?.Location.SourceTree; if (diagnosticSourceTree != null) { // Finds the relative path between editorconfig directory and diagnostic filepath. @@ -394,7 +450,7 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana // s_severityBasedEntryPattern. Both of these are considered valid severity configurations // and should be detected here. var isOptionBasedMatch = s_optionBasedEntryPattern.IsMatch(curLineText); - var isSeverityBasedMatch = _configurationKind == ConfigurationKind.Severity && + var isSeverityBasedMatch = _configurationKind != ConfigurationKind.OptionValue && !isOptionBasedMatch && s_severityBasedEntryPattern.IsMatch(curLineText); if (isOptionBasedMatch || isSeverityBasedMatch) @@ -427,22 +483,52 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana { // We found a rule configuration entry of severity based form: // "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% - var diagIdLength = -1; - if (key.StartsWith(DiagnosticOptionPrefix, StringComparison.Ordinal) && - key.EndsWith(DiagnosticOptionSuffix, StringComparison.Ordinal)) - { - diagIdLength = key.Length - (DiagnosticOptionPrefix.Length + DiagnosticOptionSuffix.Length); - } + // OR + // "dotnet_analyzer_diagnostic.severity = %severity% + // OR + // "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% - if (diagIdLength >= 0) + switch (_configurationKind) { - var diagId = key.Substring( - DiagnosticOptionPrefix.Length, - diagIdLength); - if (string.Equals(diagId, _diagnostic.Id, StringComparison.OrdinalIgnoreCase)) - { - textChange = new TextChange(curLine.Span, $"{key} = {_newSeverity}{commentValue}"); - } + case ConfigurationKind.Severity: + RoslynDebug.Assert(_diagnostic != null); + if (key.StartsWith(DiagnosticOptionPrefix, StringComparison.Ordinal) && + key.EndsWith(SeveritySuffix, StringComparison.Ordinal)) + { + var diagIdLength = key.Length - (DiagnosticOptionPrefix.Length + SeveritySuffix.Length); + var diagId = key.Substring(DiagnosticOptionPrefix.Length, diagIdLength); + if (string.Equals(diagId, _diagnostic.Id, StringComparison.OrdinalIgnoreCase)) + { + textChange = new TextChange(curLine.Span, $"{key} = {_newSeverity}{commentValue}"); + } + } + + break; + + case ConfigurationKind.BulkConfigure: + RoslynDebug.Assert(_categoryToBulkConfigure != null); + if (_categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory) + { + if (key == BulkConfigureAllAnalyzerDiagnosticsOptionKey) + { + textChange = new TextChange(curLine.Span, $"{key} = {_newSeverity}{commentValue}"); + } + } + else + { + if (key.StartsWith(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix, StringComparison.Ordinal) && + key.EndsWith(SeveritySuffix, StringComparison.Ordinal)) + { + var categoryLength = key.Length - (BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length + SeveritySuffix.Length); + var category = key.Substring(BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix.Length, categoryLength); + if (string.Equals(category, _categoryToBulkConfigure, StringComparison.OrdinalIgnoreCase)) + { + textChange = new TextChange(curLine.Span, $"{key} = {_newSeverity}{commentValue}"); + } + } + } + + break; } } } @@ -499,7 +585,7 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana // Thus, we want to keep track of whether there is an existing header that only contains [*.cs] or only // [*.vb], depending on the language. // We also keep track of the last valid header for the language. - var isLanguageAgnosticEntry = !SuppressionHelpers.IsCompilerDiagnostic(_diagnostic) && _isPerLanguage; + var isLanguageAgnosticEntry = (_diagnostic == null || !SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) && _isPerLanguage; if (isLanguageAgnosticEntry) { if ((_language.Equals(LanguageNames.CSharp) || _language.Equals(LanguageNames.VisualBasic)) && @@ -557,23 +643,37 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana return (null, lastValidHeaderSpanEnd, lastValidSpecificHeaderSpanEnd); } - private SourceText AddMissingRule( + private SourceText? AddMissingRule( SourceText result, TextLine? lastValidHeaderSpanEnd, TextLine? lastValidSpecificHeaderSpanEnd) { - // Create a new rule configuration entry for the given diagnostic ID. + // Create a new rule configuration entry for the given diagnostic ID or bulk configuration category. // If optionNameOpt and optionValueOpt are non-null, it indicates an option based diagnostic ID // which can be configured by a new entry such as: "%option_name% = %option_value%:%severity% - // Otherwise, it indicates a non-option diagnostic ID, + // Otherwise, if diagnostic is non-null, it indicates a non-option diagnostic ID, // which can be configured by a new entry such as: "dotnet_diagnostic.<%DiagnosticId%>.severity = %severity% + // Otherwise, it indicates a bulk configuration entry for default severity of a specific diagnostic category or all analyzer diagnostics, + // which can be configured by a new entry such as: + // 1. All analyzer diagnostics: "dotnet_analyzer_diagnostic.severity = %severity% + // 2. Category configuration: "dotnet_analyzer_diagnostic.category-<%DiagnosticCategory%>.severity = %severity% var newEntry = !string.IsNullOrEmpty(_optionNameOpt) && !string.IsNullOrEmpty(_newOptionValueOpt) ? $"{_optionNameOpt} = {_newOptionValueOpt}:{_newSeverity}" - : $"{DiagnosticOptionPrefix}{_diagnostic.Id}{DiagnosticOptionSuffix} = {_newSeverity}"; + : _diagnostic != null + ? $"{DiagnosticOptionPrefix}{_diagnostic.Id}{SeveritySuffix} = {_newSeverity}" + : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory + ? $"{BulkConfigureAllAnalyzerDiagnosticsOptionKey} = {_newSeverity}" + : $"{BulkConfigureAnalyzerDiagnosticsByCategoryOptionPrefix}{_categoryToBulkConfigure}{SeveritySuffix} = {_newSeverity}"; + + // Insert a new line and comment text above the new entry + var commentPrefix = _diagnostic != null + ? $"{_diagnostic.Id}: {_diagnostic.Descriptor.Title}" + : _categoryToBulkConfigure == AllAnalyzerDiagnosticsCategory + ? "Default severity for all analyzer diagnostics" + : $"Default severity for analyzer diagnostics with category '{_categoryToBulkConfigure}'"; - // Insert a new line and comment text with diagnostic title above the new entry - newEntry = $"\r\n# {_diagnostic.Id}: {_diagnostic.Descriptor.Title}\r\n{newEntry}\r\n"; + newEntry = $"\r\n# {commentPrefix}\r\n{newEntry}\r\n"; // Check if have a correct existing header for the new entry. // - If the diagnostic's isPerLanguage = true, it means the rule is valid for both C# and VB. @@ -623,7 +723,7 @@ private SourceText GetNewAnalyzerConfigDocumentText(SourceText originalText, Ana prefix += "\r\n"; } - var compilerDiagOrNotPerLang = SuppressionHelpers.IsCompilerDiagnostic(_diagnostic) || !_isPerLanguage; + var compilerDiagOrNotPerLang = (_diagnostic != null && SuppressionHelpers.IsCompilerDiagnostic(_diagnostic)) || !_isPerLanguage; if (_language.Equals(LanguageNames.CSharp) && compilerDiagOrNotPerLang) { prefix += "[*.cs]\r\n"; diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs new file mode 100644 index 00000000000..869add87c29 --- /dev/null +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.TopLevelBulkConfigureSeverityCodeAction.cs @@ -0,0 +1,33 @@ +// 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. + +#nullable enable + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeActions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity +{ + internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigurationFixProvider + { + private sealed class TopLevelBulkConfigureSeverityCodeAction : AbstractConfigurationActionWithNestedActions + { + public TopLevelBulkConfigureSeverityCodeAction(ImmutableArray nestedActions, string? category) + : base(nestedActions, + category != null + ? string.Format(FeaturesResources.Configure_severity_for_all_0_analyzers, category) + : FeaturesResources.Configure_severity_for_all_analyzers) + { + // Ensure that 'Category' based bulk configuration actions are shown above + // the 'All analyzer diagnostics' bulk configuration actions. + AdditionalPriority = category != null ? CodeActionPriority.Low : CodeActionPriority.None; + } + + internal override CodeActionPriority AdditionalPriority { get; } + + internal override bool IsBulkConfigurationAction => true; + } + } +} diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs index 1981fa945fc..3a00d6a9747 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigureSeverity/ConfigureSeverityLevelCodeFixProvider.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; @@ -12,6 +14,7 @@ using Microsoft.CodeAnalysis.Options.EditorConfig; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CodeActions.CodeAction; namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureSeverity @@ -33,7 +36,7 @@ internal sealed partial class ConfigureSeverityLevelCodeFixProvider : IConfigura public bool IsFixableDiagnostic(Diagnostic diagnostic) => !diagnostic.IsSuppressed && !SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic); - public FixAllProvider GetFixAllProvider() + public FixAllProvider? GetFixAllProvider() => null; public Task> GetFixesAsync(Document document, TextSpan span, IEnumerable diagnostics, CancellationToken cancellationToken) @@ -51,6 +54,8 @@ private static ImmutableArray GetConfigurations(Project project, IEnume } var result = ArrayBuilder.GetInstance(); + var anayzerDiagnosticsByCategory = new SortedDictionary>(); + using var disposer = ArrayBuilder.GetInstance(out var analyzerDiagnostics); foreach (var diagnostic in diagnostics) { var nestedActions = ArrayBuilder.GetInstance(); @@ -62,9 +67,49 @@ private static ImmutableArray GetConfigurations(Project project, IEnume var codeAction = new TopLevelConfigureSeverityCodeAction(diagnostic, nestedActions.ToImmutableAndFree()); result.Add(new CodeFix(project, codeAction, diagnostic)); + + // Bulk configuration is only supported for analyzer diagnostics. + if (!SuppressionHelpers.IsCompilerDiagnostic(diagnostic)) + { + // Ensure diagnostic has a valid non-empty 'Category' for category based configuration. + if (!string.IsNullOrEmpty(diagnostic.Descriptor.Category)) + { + var diagnosticsForCategory = anayzerDiagnosticsByCategory.GetOrAdd(diagnostic.Descriptor.Category, _ => ArrayBuilder.GetInstance()); + diagnosticsForCategory.Add(diagnostic); + } + + analyzerDiagnostics.Add(diagnostic); + } + } + + foreach (var (category, diagnosticsWithCategory) in anayzerDiagnosticsByCategory) + { + AddBulkConfigurationCodeFixes(diagnosticsWithCategory.ToImmutableAndFree(), category); + } + + if (analyzerDiagnostics.Count > 0) + { + AddBulkConfigurationCodeFixes(analyzerDiagnostics.ToImmutable(), category: null); } return result.ToImmutableAndFree(); + + void AddBulkConfigurationCodeFixes(ImmutableArray diagnostics, string? category) + { + var nestedActions = ArrayBuilder.GetInstance(); + foreach (var (name, value) in s_editorConfigSeverityStrings) + { + nestedActions.Add( + new SolutionChangeAction( + name, + solution => category != null + ? ConfigurationUpdater.BulkConfigureSeverityAsync(value, category, project, cancellationToken) + : ConfigurationUpdater.BulkConfigureSeverityAsync(value, project, cancellationToken))); + } + + var codeAction = new TopLevelBulkConfigureSeverityCodeAction(nestedActions.ToImmutableAndFree(), category); + result.Add(new CodeFix(project, codeAction, diagnostics)); + } } } } diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 8550c80eabb..565c93e48c7 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -691,6 +691,12 @@ Do you want to continue? Configure {0} code style + + Configure severity for all '{0}' analyzers + + + Configure severity for all analyzers + <Pending> diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index b918280008d..0dcbc2ba5dc 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -137,6 +137,16 @@ Nakonfigurovat závažnost {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Převést na LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index df7ff5c47e7..7fb70d9b3a1 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -137,6 +137,16 @@ Schweregrad "{0}" konfigurieren + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ In LINQ konvertieren diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 67146a428f5..8b9b43ff60b 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -137,6 +137,16 @@ Configurar la gravedad de {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Convertir a LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index 13fffbef89d..8d0fcfc8626 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -137,6 +137,16 @@ Configurer la gravité {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Convertir en LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 096df1e5b97..a76f3290585 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -137,6 +137,16 @@ Configura la gravità di {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Converti in LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index 0aa1e3ba776..47844cf9a8f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -137,6 +137,16 @@ {0} の重要度の構成 + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ LINQ に変換 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 32f2f67c3ea..96d6645a3be 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -137,6 +137,16 @@ {0} 심각도 구성 + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ LINQ로 변환 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index efeac169a0e..60f1452b7d0 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -137,6 +137,16 @@ Konfiguruj ważność {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Konwertuj na składnię LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index 1f139e9eed4..1e81a951cf6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -137,6 +137,16 @@ Configurar severidade de {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Converter para LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 82a9433a12a..8e410a398a7 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -137,6 +137,16 @@ Настройка серьезности {0} + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ Преобразовать в LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 162cca9b9c9..647ba65dd53 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -137,6 +137,16 @@ {0} önem derecesini yapılandır + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ LINQ to dönüştürme diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index cb7886c3fdc..107d7480a79 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -137,6 +137,16 @@ 配置 {0} 严重性 + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ 转换为 LINQ diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index d7340a0ebc3..4e9005b029b 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -137,6 +137,16 @@ 設定 {0} 嚴重性 + + Configure severity for all '{0}' analyzers + Configure severity for all '{0}' analyzers + + + + Configure severity for all analyzers + Configure severity for all analyzers + + Convert to LINQ 轉換至 LINQ diff --git a/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpCodeActions.cs b/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpCodeActions.cs index 77908404d5f..e67a1db79ad 100644 --- a/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpCodeActions.cs +++ b/src/VisualStudio/IntegrationTest/IntegrationTests/CSharp/CSharpCodeActions.cs @@ -696,6 +696,18 @@ static void Main(string[] args) "Suggestion", "Warning", "Error", + "Configure severity for all 'Style' analyzers", + "None", + "Silent", + "Suggestion", + "Warning", + "Error", + "Configure severity for all analyzers", + "None", + "Silent", + "Suggestion", + "Warning", + "Error", }; VisualStudio.Editor.Verify.CodeActions(expectedItems, ensureExpectedItemsAreOrdered: true); diff --git a/src/Workspaces/Core/Portable/CodeFixes/CodeFix.cs b/src/Workspaces/Core/Portable/CodeFixes/CodeFix.cs index 928ba3c1a62..e82863e6583 100644 --- a/src/Workspaces/Core/Portable/CodeFixes/CodeFix.cs +++ b/src/Workspaces/Core/Portable/CodeFixes/CodeFix.cs @@ -51,7 +51,7 @@ internal CodeFix(Project project, CodeAction action, Diagnostic diagnostic) internal CodeFix(Project project, CodeAction action, ImmutableArray diagnostics) { - Debug.Assert(!diagnostics.IsDefault); + Debug.Assert(!diagnostics.IsDefaultOrEmpty); Project = project; Action = action; Diagnostics = diagnostics; -- GitLab