File: GenerateConstructors\AbstractGenerateConstructorsCodeRefactoringProvider.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.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Features.Intents;
using Microsoft.CodeAnalysis.GenerateDefaultConstructors;
using Microsoft.CodeAnalysis.GenerateFromMembers;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PickMembers;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.GenerateConstructors;
 
using static GenerateFromMembersHelpers;
 
/// <summary>
/// This <see cref="CodeRefactoringProvider"/> is responsible for allowing a user to pick a set of members from a class
/// or struct, and then generate a constructor for that takes in matching parameters and assigns them to those members.
/// The members can be picked using a actual selection in the editor, or they can be picked using a picker control that
/// will then display all the viable members and allow the user to pick which ones they want to use.
/// <para/>
/// This <see cref="CodeRefactoringProvider"/> also gives users a way to generate constructors for a derived type that
/// delegate to a base type.  For all accessible constructors in the base type, the user will be offered to create a
/// constructor in the derived type with the same signature if they don't already have one.  This way, a user can
/// override a type and easily create all the forwarding constructors.
/// <para/>
/// Importantly, this type is not responsible for generating constructors when the user types something like "new
/// MyType(x, y, z)".
/// </summary>
/// <remarks>
/// For testing purposes only.
/// </remarks>
internal abstract partial class AbstractGenerateConstructorsCodeRefactoringProvider(IPickMembersService? pickMembersService_forTesting)
    : CodeRefactoringProvider, IIntentProvider
{
    public record GenerateConstructorIntentData(Accessibility? Accessibility);
 
    private const string AddNullChecksId = nameof(AddNullChecksId);
 
    private readonly IPickMembersService? _pickMembersService_forTesting = pickMembersService_forTesting;
 
    protected AbstractGenerateConstructorsCodeRefactoringProvider() : this(null)
    {
    }
 
    protected abstract bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType);
    protected abstract string ToDisplayString(IParameterSymbol parameter, SymbolDisplayFormat format);
    protected abstract ValueTask<bool> PrefersThrowExpressionAsync(Document document, CancellationToken cancellationToken);
    protected abstract IFieldSymbol? TryMapToWritableInstanceField(IPropertySymbol property, CancellationToken cancellationToken);
 
    public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        return ComputeRefactoringsAsync(
            context.Document,
            context.Span,
            context.RegisterRefactoring,
            actions => context.RegisterRefactorings(actions),
            desiredAccessibility: null,
            context.CancellationToken);
    }
 
    public async Task<ImmutableArray<IntentProcessorResult>> ComputeIntentAsync(
        Document priorDocument, TextSpan priorSelection, Document currentDocument, IntentDataProvider intentDataProvider, CancellationToken cancellationToken)
    {
        var accessibility = intentDataProvider.GetIntentData<GenerateConstructorIntentData>()?.Accessibility;
 
        using var _1 = ArrayBuilder<CodeAction>.GetInstance(out var actions);
        await ComputeRefactoringsAsync(
            priorDocument,
            priorSelection,
            (singleAction, applicableToSpan) => actions.Add(singleAction),
            actions.AddRange,
            desiredAccessibility: accessibility,
            cancellationToken).ConfigureAwait(false);
 
        if (actions.IsEmpty)
        {
            return [];
        }
 
        // The refactorings returned will be in the following order (if available)
        // FieldDelegatingCodeAction, ConstructorDelegatingCodeAction, GenerateConstructorWithDialogCodeAction
        using var _2 = ArrayBuilder<IntentProcessorResult>.GetInstance(out var results);
        foreach (var action in actions)
        {
            // Intents don't current support progress.
            var intentResult = await GetIntentProcessorResultAsync(
                priorDocument, action, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false);
            results.AddIfNotNull(intentResult);
        }
 
        return results.ToImmutableAndClear();
 
        static async Task<IntentProcessorResult?> GetIntentProcessorResultAsync(
            Document priorDocument, CodeAction codeAction, IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
        {
            var operations = await GetCodeActionOperationsAsync(
                priorDocument.Project.Solution, codeAction, progressTracker, cancellationToken).ConfigureAwait(false);
 
            // Generate ctor will only return an ApplyChangesOperation or potentially document navigation actions.
            // We can only return edits, so we only care about the ApplyChangesOperation.
            var applyChangesOperation = operations.OfType<ApplyChangesOperation>().SingleOrDefault();
            if (applyChangesOperation == null)
            {
                return null;
            }
 
            var type = codeAction.GetType();
            return new IntentProcessorResult(applyChangesOperation.ChangedSolution, [priorDocument.Id], codeAction.Title, type.Name);
        }
 
        static async Task<ImmutableArray<CodeActionOperation>> GetCodeActionOperationsAsync(
            Solution originalSolution,
            CodeAction action,
            IProgress<CodeAnalysisProgress> progressTracker,
            CancellationToken cancellationToken)
        {
            if (action is GenerateConstructorWithDialogCodeAction dialogAction)
            {
                // Usually applying this code action pops up a dialog allowing the user to choose which options.
                // We can't do that here, so instead we just take the defaults until we have more intent data.
                var options = new PickMembersResult(
                    dialogAction.ViableMembers,
                    dialogAction.PickMembersOptions,
                    selectedAll: true);
                var operations = await dialogAction.GetOperationsAsync(originalSolution, options, progressTracker, cancellationToken).ConfigureAwait(false);
                return operations == null ? [] : operations.ToImmutableArray();
            }
            else
            {
                return await action.GetOperationsAsync(originalSolution, progressTracker, cancellationToken).ConfigureAwait(false);
            }
        }
    }
 
    private async Task ComputeRefactoringsAsync(
        Document document,
        TextSpan textSpan,
        Action<CodeAction, TextSpan> registerSingleAction,
        Action<ImmutableArray<CodeAction>> registerMultipleActions,
        Accessibility? desiredAccessibility,
        CancellationToken cancellationToken)
    {
        // TODO: https://github.com/dotnet/roslyn/issues/5778. Not supported in REPL for now.
        if (document.Project.IsSubmission)
            return;
 
        if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles)
            return;
 
        // First, see if we can offer a specific constructor based on what the user has selected, or has their caret on.
        var actions = await GenerateConstructorFromMembersAsync(
            document, textSpan, addNullChecks: false, desiredAccessibility, cancellationToken).ConfigureAwait(false);
        if (!actions.IsDefaultOrEmpty)
        {
            registerMultipleActions(actions);
        }
        else if (textSpan.IsEmpty)
        {
            // If that produced nothing, and the user hasn't selected anything explicitly, then also offer the option to
            // generate a constructor with a dialog.
            var nonSelectionAction = await HandleNonSelectionAsync(document, textSpan, desiredAccessibility, cancellationToken).ConfigureAwait(false);
            if (nonSelectionAction is var (codeAction, applicableToSpan))
                registerSingleAction(codeAction, applicableToSpan);
        }
 
        // Finally, offer to generate default constructors that delegate to the base type if any are missing.
        var defaultConstructorService = document.GetRequiredLanguageService<IGenerateDefaultConstructorsService>();
        var defaultConstructorActions = await defaultConstructorService.GenerateDefaultConstructorsAsync(
            document, textSpan, forRefactoring: true, cancellationToken).ConfigureAwait(false);
 
        registerMultipleActions(defaultConstructorActions);
    }
 
    private async Task<(CodeAction CodeAction, TextSpan ApplicableToSpan)?> HandleNonSelectionAsync(
        Document document,
        TextSpan textSpan,
        Accessibility? desiredAccessibility,
        CancellationToken cancellationToken)
    {
        var helpers = document.GetRequiredLanguageService<IRefactoringHelpersService>();
        var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        // We offer the refactoring when the user is either on the header of a class/struct,
        // or if they're between any members of a class/struct and are on a blank line.
        if (!helpers.IsOnTypeHeader(root, textSpan.Start, out var typeDeclaration) &&
            !helpers.IsBetweenTypeMembers(sourceText, root, textSpan.Start, out typeDeclaration))
        {
            return null;
        }
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        // Only supported on classes/structs.
        var containingType = semanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken: cancellationToken) as INamedTypeSymbol;
        if (containingType?.TypeKind is not TypeKind.Class and not TypeKind.Struct)
        {
            return null;
        }
 
        // No constructors for static classes.
        if (containingType.IsStatic)
        {
            return null;
        }
 
        // Find all the possible writable instance fields/properties.  If there are any, then
        // show a dialog to the user to select the ones they want.  Otherwise, if there are none
        // don't offer to generate anything.
        var viableMembers = containingType.GetMembers().WhereAsArray(IsWritableInstanceFieldOrProperty);
        if (viableMembers.Length == 0)
        {
            return null;
        }
 
        // We shouldn't offer a refactoring if the compilation doesn't contain the ArgumentNullException type,
        // as we use it later on in our computations.
        var argumentNullExceptionType = typeof(ArgumentNullException).FullName;
        if (argumentNullExceptionType is null || semanticModel.Compilation.GetTypeByMetadataName(argumentNullExceptionType) is null)
        {
            return null;
        }
 
        using var _ = ArrayBuilder<PickMembersOption>.GetInstance(out var pickMemberOptions);
        var canAddNullCheck = viableMembers.Any(
            static m => m.GetSymbolType().CanAddNullCheck());
 
        if (canAddNullCheck)
        {
            // ILegacyGlobalOptionsWorkspaceService is not provided in LSP, so don't give the code action with Dialog if it is null
            var globalOptions = document.Project.Solution.Services.GetService<ILegacyGlobalOptionsWorkspaceService>();
            if (globalOptions == null)
            {
                return null;
            }
 
            var optionValue = globalOptions.GetGenerateConstructorFromMembersOptionsAddNullChecks(document.Project.Language);
            pickMemberOptions.Add(new PickMembersOption(
                AddNullChecksId,
                FeaturesResources.Add_null_checks,
                optionValue));
        }
 
        return (new GenerateConstructorWithDialogCodeAction(
                this, document, textSpan, containingType, desiredAccessibility, viableMembers,
                pickMemberOptions.ToImmutable()), typeDeclaration.Span);
    }
 
    public async Task<ImmutableArray<CodeAction>> GenerateConstructorFromMembersAsync(
        Document document, TextSpan textSpan, bool addNullChecks, Accessibility? desiredAccessibility, CancellationToken cancellationToken)
    {
        using (Logger.LogBlock(FunctionId.Refactoring_GenerateFromMembers_GenerateConstructorFromMembers, cancellationToken))
        {
            var info = await GetSelectedMemberInfoAsync(document, textSpan, allowPartialSelection: true, cancellationToken).ConfigureAwait(false);
            if (info != null)
            {
                var state = await State.TryGenerateAsync(this, document, textSpan, info.ContainingType, desiredAccessibility, info.SelectedMembers, cancellationToken).ConfigureAwait(false);
                if (state != null && state.MatchingConstructor == null)
                    return GetCodeActions(document, state, addNullChecks);
            }
 
            return default;
        }
    }
 
    private ImmutableArray<CodeAction> GetCodeActions(Document document, State state, bool addNullChecks)
    {
        using var result = TemporaryArray<CodeAction>.Empty;
 
        result.Add(new FieldDelegatingCodeAction(this, document, state, addNullChecks));
        if (state.DelegatedConstructor != null)
            result.Add(new ConstructorDelegatingCodeAction(this, document, state, addNullChecks));
 
        return result.ToImmutableAndClear();
    }
 
    private static async Task<Document> AddNavigationAnnotationAsync(Document document, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var nodes = root.GetAnnotatedNodes(CodeGenerator.Annotation);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        foreach (var node in nodes)
        {
            var parameterList = syntaxFacts.GetParameterList(node);
            if (parameterList != null)
            {
                var closeParen = parameterList.GetLastToken();
                var newRoot = root.ReplaceToken(closeParen, closeParen.WithAdditionalAnnotations(NavigationAnnotation.Create()));
                return document.WithSyntaxRoot(newRoot);
            }
        }
 
        return document;
    }
}