File: CodeFixes\Configuration\ConfigureCodeStyle\ConfigureCodeStyleOptionCodeFixProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
 
namespace Microsoft.CodeAnalysis.CodeFixes.Configuration.ConfigureCodeStyle;
 
[ExportConfigurationFixProvider(PredefinedConfigurationFixProviderNames.ConfigureCodeStyleOption, LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
[ExtensionOrder(Before = PredefinedConfigurationFixProviderNames.ConfigureSeverity)]
[ExtensionOrder(After = PredefinedConfigurationFixProviderNames.Suppression)]
internal sealed partial class ConfigureCodeStyleOptionCodeFixProvider : IConfigurationFixProvider
{
    private static readonly ImmutableArray<bool> s_boolValues = [true, false];
 
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public ConfigureCodeStyleOptionCodeFixProvider()
    {
    }
 
    public bool IsFixableDiagnostic(Diagnostic diagnostic)
    {
        // We only offer fix for configurable code style diagnostics which have one of more editorconfig based storage locations.
        // Also skip suppressed diagnostics defensively, though the code fix engine should ideally never call us for suppressed diagnostics.
        if (diagnostic.IsSuppressed ||
            SuppressionHelpers.IsNotConfigurableDiagnostic(diagnostic) ||
            diagnostic.Location.SourceTree == null)
        {
            return false;
        }
 
        var language = diagnostic.Location.SourceTree.Options.Language;
        return IDEDiagnosticIdToOptionMappingHelper.TryGetMappedOptions(diagnostic.Id, language, out _);
    }
 
    public FixAllProvider? GetFixAllProvider()
        => null;
 
    public Task<ImmutableArray<CodeFix>> GetFixesAsync(TextDocument document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        => Task.FromResult(GetConfigurations(document.Project, diagnostics));
 
    public Task<ImmutableArray<CodeFix>> GetFixesAsync(Project project, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        => Task.FromResult(GetConfigurations(project, diagnostics));
 
    private static ImmutableArray<CodeFix> GetConfigurations(Project project, IEnumerable<Diagnostic> diagnostics)
    {
        var result = ArrayBuilder<CodeFix>.GetInstance();
        foreach (var diagnostic in diagnostics)
        {
            // First get all the relevant code style options for the diagnostic.
            var codeStyleOptions = ConfigurationUpdater.GetCodeStyleOptionsForDiagnostic(diagnostic, project);
            if (codeStyleOptions.IsEmpty)
            {
                continue;
            }
 
            // For each code style option, create a top level code action with nested code actions for every valid option value.
            // For example, if the option value is CodeStyleOption<bool>, we will have two nested actions, one for 'true' setting and one
            // for 'false' setting. If the option value is CodeStyleOption<SomeEnum>, we will have a nested action for each enum field.
            using var _ = ArrayBuilder<CodeAction>.GetInstance(out var nestedActions);
            var hasMultipleOptions = codeStyleOptions.Length > 1;
            foreach (var option in codeStyleOptions)
            {
                var topLevelAction = GetCodeActionForCodeStyleOption(option, diagnostic, hasMultipleOptions);
                if (topLevelAction != null)
                {
                    nestedActions.Add(topLevelAction);
                }
            }
 
            if (nestedActions.Count != 0)
            {
                // Wrap actions by another level if the diagnostic ID has multiple associated code style options to reduce clutter.
                var resultCodeAction = nestedActions.Count > 1
                    ? new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable())
                    : nestedActions.Single();
 
                result.Add(new CodeFix(project, resultCodeAction, diagnostic));
            }
        }
 
        return result.ToImmutableAndFree();
 
        // Local functions
        TopLevelConfigureCodeStyleOptionCodeAction? GetCodeActionForCodeStyleOption(IOption2 option, Diagnostic diagnostic, bool hasMultipleOptions)
        {
            // Add a code action for every valid value of the given code style option.
            // We only support light-bulb configuration of code style options with boolean or enum values.
 
            using var _ = ArrayBuilder<CodeAction>.GetInstance(out var nestedActions);
 
            // Try to get the parsed editorconfig string representation of the new code style option value
            var optionName = option.Definition.ConfigName;
            var defaultValue = (ICodeStyleOption2?)option.DefaultValue;
            Contract.ThrowIfNull(defaultValue);
 
            if (defaultValue.Value is bool)
            {
                foreach (var boolValue in s_boolValues)
                {
                    AddCodeActionWithOptionValue(defaultValue, boolValue);
                }
            }
            else if (defaultValue.Value?.GetType() is Type t && t.IsEnum)
            {
                foreach (var enumValue in Enum.GetValues(t))
                {
                    AddCodeActionWithOptionValue(defaultValue, enumValue!);
                }
            }
 
            if (nestedActions.Count > 0)
            {
                // If this is not a unique code style option for the diagnostic, use the optionName as the code action title.
                // In that case, we will already have a containing top level action for the diagnostic.
                // Otherwise, use the diagnostic information in the title.
                return hasMultipleOptions
                    ? new TopLevelConfigureCodeStyleOptionCodeAction(optionName, nestedActions.ToImmutable())
                    : new TopLevelConfigureCodeStyleOptionCodeAction(diagnostic, nestedActions.ToImmutable());
            }
 
            return null;
 
            // Local functions
            void AddCodeActionWithOptionValue(ICodeStyleOption2 codeStyleOption, object newValue)
            {
                // Create a new code style option value with the newValue
                var configuredCodeStyleOption = codeStyleOption.WithValue(newValue);
                var optionValue = option.Definition.Serializer.Serialize(configuredCodeStyleOption);
 
                // Add code action to configure the optionValue.
                nestedActions.Add(
                    SolutionChangeAction.Create(
                        optionValue,
                        cancellationToken => ConfigurationUpdater.ConfigureCodeStyleOptionAsync(optionName, optionValue, diagnostic, option.IsPerLanguage, project, cancellationToken),
                        optionValue));
            }
        }
    }
}