File: AddConstructorParametersFromMembers\AddConstructorParametersFromMembersCodeRefactoringProvider.AddConstructorParametersCodeAction.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.GenerateFromMembers;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.AddConstructorParametersFromMembers;
 
using static GenerateFromMembersHelpers;
 
internal sealed partial class AddConstructorParametersFromMembersCodeRefactoringProvider
{
    private sealed class AddConstructorParametersCodeAction(
        Document document,
        CodeGenerationContextInfo info,
        ConstructorCandidate constructorCandidate,
        bool useSubMenuName) : CodeAction
    {
        private readonly Document _document = document;
        private readonly CodeGenerationContextInfo _info = info;
 
        private IMethodSymbol Constructor => constructorCandidate.Constructor;
        private ImmutableArray<(IParameterSymbol parameter, ISymbol fieldOrPropert)> MissingParameters => constructorCandidate.MissingParametersAndMembers;
 
        /// <summary>
        /// If there is more than one constructor, the suggested actions will be split into two sub menus,
        /// one for regular parameters and one for optional. This boolean is used by the Title property
        /// to determine if the code action should be given the complete title or the sub menu title
        /// </summary>
        private readonly bool _useSubMenuName = useSubMenuName;
 
        protected override async Task<Solution?> GetChangedSolutionAsync(
            IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
        {
            var declarationService = _document.GetRequiredLanguageService<ISymbolDeclarationService>();
            var constructor = declarationService.GetDeclarations(
                Constructor).Select(r => r.GetSyntax(cancellationToken)).First();
 
            return !Constructor.IsPrimaryConstructor()
                ? AddParametersToRegularConstructor(constructor, cancellationToken)
                : await AddParametersAndInitializersToPrimaryConstructorAsync(constructor, cancellationToken).ConfigureAwait(false);
        }
 
        private SyntaxNode GetNewConstructor(SyntaxNode oldConstructor, CancellationToken cancellationToken)
        {
            var codeGenerator = _document.GetRequiredLanguageService<ICodeGenerationService>();
            var newConstructor = codeGenerator.AddParameters(
                oldConstructor, MissingParameters.SelectAsArray(t => t.parameter), _info, cancellationToken);
 
            return newConstructor;
        }
 
        private Solution AddParametersToRegularConstructor(SyntaxNode constructor, CancellationToken cancellationToken)
        {
            var codeGenerator = _document.GetRequiredLanguageService<ICodeGenerationService>();
 
            // For regular constructors, add assignment statements
            var newConstructor = GetNewConstructor(constructor, cancellationToken);
            newConstructor = codeGenerator
                .AddStatements(newConstructor, CreateAssignStatements(), _info, cancellationToken)
                .WithAdditionalAnnotations(Formatter.Annotation);
 
            var syntaxTree = constructor.SyntaxTree;
            var newRoot = syntaxTree.GetRoot(cancellationToken).ReplaceNode(constructor, newConstructor);
 
            // Make sure we get the document that contains the constructor we just updated
            var constructorDocument = _document.Project.GetDocument(syntaxTree);
            Contract.ThrowIfNull(constructorDocument);
 
            return constructorDocument.WithSyntaxRoot(newRoot).Project.Solution;
        }
 
        private async Task<Solution> AddParametersAndInitializersToPrimaryConstructorAsync(
            SyntaxNode constructor,
            CancellationToken cancellationToken)
        {
            // For primary constructors, we need to:
            //
            // 1. Add initializers to the properties/fields
            // 2. Update the primary constructor with new parameters
            //
            // We need to do it in this order so that we see the adjustment to the properties/fields when we're updating
            // the primary constructor that wraps them all.
            var newConstructor = GetNewConstructor(constructor, cancellationToken);
 
            var solution = _document.Project.Solution;
            var solutionEditor = new SolutionEditor(solution);
 
            // First, update the primary constructor declaration with the new parameters
            var oldConstructor = _document.GetRequiredLanguageService<ISymbolDeclarationService>()
                .GetDeclarations(Constructor)
                .Select(r => r.GetSyntax(cancellationToken))
                .First();
 
            var oldConstructorSyntaxTree = oldConstructor.SyntaxTree;
 
            foreach (var (parameter, member) in MissingParameters)
            {
                await AddInitializerToMemberAsync(
                    solutionEditor, member, parameter, cancellationToken).ConfigureAwait(false);
            }
 
            var syntaxTree = oldConstructor.SyntaxTree;
            var documentToUpdate = solution.GetRequiredDocument(syntaxTree);
            var editor = await solutionEditor.GetDocumentEditorAsync(documentToUpdate.Id, cancellationToken).ConfigureAwait(false);
 
            editor.ReplaceNode(
                oldConstructor,
                (currentOldConstructor, _) =>
                {
                    var newConstructor = GetNewConstructor(currentOldConstructor, cancellationToken);
                    return newConstructor.WithAdditionalAnnotations(Formatter.Annotation);
                });
 
            return solutionEditor.GetChangedSolution();
        }
 
        private async Task AddInitializerToMemberAsync(
            SolutionEditor solutionEditor,
            ISymbol member,
            IParameterSymbol parameter,
            CancellationToken cancellationToken)
        {
            var solution = solutionEditor.OriginalSolution;
            var generator = _document.GetRequiredLanguageService<SyntaxGenerator>();
 
            if (member.DeclaringSyntaxReferences is not [var syntaxRef, ..])
                return;
 
            var memberSyntax = syntaxRef.GetSyntax(cancellationToken);
            var memberDocument = solution.GetRequiredDocument(memberSyntax.SyntaxTree);
            var memberEditor = await solutionEditor.GetDocumentEditorAsync(memberDocument.Id, cancellationToken).ConfigureAwait(false);
 
            var equalsValueClause = generator.SyntaxGeneratorInternal.EqualsValueClause(
                generator.IdentifierName(parameter.Name));
 
            // Add the initializer to the member - use different methods for properties vs fields
            var newMemberSyntax = member is IPropertySymbol
                ? generator.SyntaxGeneratorInternal.WithPropertyInitializer(memberSyntax, equalsValueClause)
                : generator.WithInitializer(memberSyntax, equalsValueClause);
 
            memberEditor.ReplaceNode(memberSyntax, newMemberSyntax);
        }
 
        private IEnumerable<SyntaxNode> CreateAssignStatements()
        {
            var factory = _document.GetRequiredLanguageService<SyntaxGenerator>();
            foreach (var (parameter, fieldOrProperty) in MissingParameters)
            {
                yield return factory.ExpressionStatement(
                    factory.AssignmentStatement(
                        factory.MemberAccessExpression(
                            factory.ThisExpression(),
                            factory.IdentifierName(fieldOrProperty.Name)),
                        factory.IdentifierName(parameter.Name)));
            }
        }
 
        public override string Title
        {
            get
            {
                var parameters = Constructor.Parameters.Select(p => p.ToDisplayString(SimpleFormat));
                var parameterString = string.Join(", ", parameters);
                var signature = $"{this.Constructor.ContainingType.Name}({parameterString})";
 
                if (_useSubMenuName)
                    return string.Format(CodeFixesResources.Add_to_0, signature);
 
                return MissingParameters[0].parameter.IsOptional
                    ? string.Format(FeaturesResources.Add_optional_parameters_to_0, signature)
                    : string.Format(FeaturesResources.Add_parameters_to_0, signature);
            }
        }
 
        /// <summary>
        /// A metadata name used by telemetry to distinguish between the different kinds of this code action.
        /// This code action will perform 2 different actions depending on if missing parameters can be optional.
        /// 
        /// In this case we don't want to use the title as it depends on the class name for the ctor.
        /// </summary>
        internal string ActionName => MissingParameters[0].parameter.IsOptional
            ? nameof(FeaturesResources.Add_optional_parameters_to_0)
            : nameof(FeaturesResources.Add_parameters_to_0);
    }
}