File: InitializeParameter\AbstractInitializeMemberFromParameterCodeRefactoringProviderMemberCreation.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Naming;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.InitializeParameter;
 
using static InitializeParameterHelpersCore;
 
internal abstract partial class AbstractInitializeMemberFromParameterCodeRefactoringProvider<
    TTypeDeclarationSyntax,
    TParameterSyntax,
    TStatementSyntax,
    TExpressionSyntax> : AbstractInitializeParameterCodeRefactoringProvider<
        TTypeDeclarationSyntax,
        TParameterSyntax,
        TStatementSyntax,
        TExpressionSyntax>
    where TTypeDeclarationSyntax : SyntaxNode
    where TParameterSyntax : SyntaxNode
    where TStatementSyntax : SyntaxNode
    where TExpressionSyntax : SyntaxNode
{
    protected abstract SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement);
    protected abstract Accessibility DetermineDefaultFieldAccessibility(INamedTypeSymbol containingType);
    protected abstract Accessibility DetermineDefaultPropertyAccessibility();
    protected abstract SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken);
    protected abstract SyntaxNode RemoveThrowNotImplemented(SyntaxNode propertySyntax);
    protected abstract bool TryUpdateTupleAssignment(IBlockOperation? blockStatement, IParameterSymbol parameter, ISymbol fieldOrProperty, SyntaxEditor editor);
 
    protected sealed override Task<ImmutableArray<CodeAction>> GetRefactoringsForAllParametersAsync(
        Document document, SyntaxNode functionDeclaration, IMethodSymbol method, IBlockOperation? blockStatementOpt,
        ImmutableArray<SyntaxNode> listOfParameterNodes, TextSpan parameterSpan,
        CancellationToken cancellationToken)
    {
        return SpecializedTasks.EmptyImmutableArray<CodeAction>();
    }
 
    protected sealed override async Task<ImmutableArray<CodeAction>> GetRefactoringsForSingleParameterAsync(
        Document document,
        TParameterSyntax parameterSyntax,
        IParameterSymbol parameter,
        SyntaxNode constructorDeclaration,
        IMethodSymbol method,
        IBlockOperation? blockStatement,
        CancellationToken cancellationToken)
    {
        // Only supported for constructor parameters.
        if (method.MethodKind != MethodKind.Constructor)
            return [];
 
        var typeDeclaration = constructorDeclaration.GetAncestor<TTypeDeclarationSyntax>();
        if (typeDeclaration == null)
            return [];
 
        // See if we're already assigning this parameter to a field/property in this type. If so, there's nothing
        // more for us to do.
        var assignmentStatement = TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatement);
        if (assignmentStatement != null)
            return [];
 
        // Haven't initialized any fields/properties with this parameter.  Offer to assign
        // to an existing matching field/prop if we can find one, or add a new field/prop
        // if we can't.
 
        var rules = await document.GetNamingRulesAsync(cancellationToken).ConfigureAwait(false);
        var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules);
        if (parameterNameParts.BaseName == "")
            return [];
 
        var (fieldOrProperty, isThrowNotImplementedProperty) = await TryFindMatchingUninitializedFieldOrPropertySymbolAsync(
            document, parameter, blockStatement, rules, parameterNameParts.BaseNameParts, cancellationToken).ConfigureAwait(false);
 
        if (fieldOrProperty != null)
        {
            return HandleExistingFieldOrProperty(
                document, parameter, constructorDeclaration, blockStatement, fieldOrProperty, isThrowNotImplementedProperty);
        }
 
        return await HandleNoExistingFieldOrPropertyAsync(
            document, parameter, constructorDeclaration,
            method, blockStatement, rules, cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<ImmutableArray<CodeAction>> HandleNoExistingFieldOrPropertyAsync(
        Document document,
        IParameterSymbol parameter,
        SyntaxNode constructorDeclaration,
        IMethodSymbol method,
        IBlockOperation? blockStatement,
        ImmutableArray<NamingRule> rules,
        CancellationToken cancellationToken)
    {
        // Didn't find a field/prop that this parameter could be assigned to.
        // Offer to create new one and assign to that.
        using var _ = ArrayBuilder<CodeAction>.GetInstance(out var allActions);
 
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        var (fieldAction, propertyAction) = AddSpecificParameterInitializationActions(
            document, parameter, constructorDeclaration, blockStatement, rules, formattingOptions.AccessibilityModifiersRequired);
 
        // Check if the surrounding parameters are assigned to another field in this class.  If so, offer to
        // make this parameter into a field as well.  Otherwise, default to generating a property
        var siblingFieldOrProperty = TryFindSiblingFieldOrProperty(parameter, blockStatement);
        if (siblingFieldOrProperty is IFieldSymbol)
        {
            allActions.Add(fieldAction);
            allActions.Add(propertyAction);
        }
        else
        {
            allActions.Add(propertyAction);
            allActions.Add(fieldAction);
        }
 
        var (allFieldsAction, allPropertiesAction) = AddAllParameterInitializationActions(
            document, constructorDeclaration, method, blockStatement, rules, formattingOptions.AccessibilityModifiersRequired);
 
        if (allFieldsAction != null && allPropertiesAction != null)
        {
            if (siblingFieldOrProperty is IFieldSymbol)
            {
                allActions.Add(allFieldsAction);
                allActions.Add(allPropertiesAction);
            }
            else
            {
                allActions.Add(allPropertiesAction);
                allActions.Add(allFieldsAction);
            }
        }
 
        return allActions.ToImmutableAndClear();
    }
 
    private (CodeAction? fieldAction, CodeAction? propertyAction) AddAllParameterInitializationActions(
        Document document,
        SyntaxNode constructorDeclaration,
        IMethodSymbol method,
        IBlockOperation? blockStatement,
        ImmutableArray<NamingRule> rules,
        AccessibilityModifiersRequired accessibilityModifiersRequired)
    {
        if (blockStatement == null)
            return default;
 
        var parameters = GetParametersWithoutAssociatedMembers(blockStatement, rules, method);
 
        if (parameters.Length < 2)
            return default;
 
        var fields = parameters.SelectAsArray(p => (ISymbol)CreateField(p, accessibilityModifiersRequired, rules));
        var properties = parameters.SelectAsArray(p => (ISymbol)CreateProperty(p, accessibilityModifiersRequired, rules));
 
        var allFieldsAction = CodeAction.Create(
            FeaturesResources.Create_and_assign_remaining_as_fields,
            c => AddAllSymbolInitializationsAsync(
                document, constructorDeclaration, blockStatement, parameters, fields, c),
            nameof(FeaturesResources.Create_and_assign_remaining_as_fields));
        var allPropertiesAction = CodeAction.Create(
            FeaturesResources.Create_and_assign_remaining_as_properties,
            c => AddAllSymbolInitializationsAsync(
                document, constructorDeclaration, blockStatement, parameters, properties, c),
            nameof(FeaturesResources.Create_and_assign_remaining_as_properties));
 
        return (allFieldsAction, allPropertiesAction);
    }
 
    private (CodeAction fieldAction, CodeAction propertyAction) AddSpecificParameterInitializationActions(
        Document document,
        IParameterSymbol parameter,
        SyntaxNode constructorDeclaration,
        IBlockOperation? blockStatement,
        ImmutableArray<NamingRule> rules,
        AccessibilityModifiersRequired accessibilityModifiersRequired)
    {
        var field = CreateField(parameter, accessibilityModifiersRequired, rules);
        var property = CreateProperty(parameter, accessibilityModifiersRequired, rules);
 
        // we're generating the field or property, so we don't have to handle throwing versions of them.
        var isThrowNotImplementedProperty = false;
 
        var fieldAction = CodeAction.Create(
            string.Format(FeaturesResources.Create_and_assign_field_0, field.Name),
            c => AddSingleSymbolInitializationAsync(document, constructorDeclaration, blockStatement, parameter, field, isThrowNotImplementedProperty, c),
            nameof(FeaturesResources.Create_and_assign_field_0) + "_" + field.Name);
        var propertyAction = CodeAction.Create(
            string.Format(FeaturesResources.Create_and_assign_property_0, property.Name),
            c => AddSingleSymbolInitializationAsync(document, constructorDeclaration, blockStatement, parameter, property, isThrowNotImplementedProperty, c),
            nameof(FeaturesResources.Create_and_assign_property_0) + "_" + property.Name);
 
        return (fieldAction, propertyAction);
    }
 
    private static ImmutableArray<IParameterSymbol> GetParametersWithoutAssociatedMembers(
        IBlockOperation? blockStatement,
        ImmutableArray<NamingRule> rules,
        IMethodSymbol method)
    {
        using var _ = ArrayBuilder<IParameterSymbol>.GetInstance(out var result);
 
        foreach (var parameter in method.Parameters)
        {
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules);
            if (parameterNameParts.BaseName == "")
                continue;
 
            var assignmentOp = TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatement);
            if (assignmentOp != null)
                continue;
 
            result.Add(parameter);
        }
 
        return result.ToImmutableAndClear();
    }
 
    private ImmutableArray<CodeAction> HandleExistingFieldOrProperty(
        Document document,
        IParameterSymbol parameter,
        SyntaxNode functionDeclaration,
        IBlockOperation? blockStatement,
        ISymbol fieldOrProperty,
        bool isThrowNotImplementedProperty)
    {
        // Found a field/property that this parameter should be assigned to.
        // Just offer the simple assignment to it.
 
        var resource = fieldOrProperty.Kind == SymbolKind.Field
            ? FeaturesResources.Initialize_field_0
            : FeaturesResources.Initialize_property_0;
 
        var title = string.Format(resource, fieldOrProperty.Name);
 
        return [CodeAction.Create(
            title,
            c => AddSingleSymbolInitializationAsync(
                document, functionDeclaration, blockStatement, parameter, fieldOrProperty, isThrowNotImplementedProperty, c),
            title)];
    }
 
    private static ISymbol? TryFindSiblingFieldOrProperty(
        IParameterSymbol parameter, IBlockOperation? blockStatement)
    {
        foreach (var (siblingParam, _) in GetSiblingParameters(parameter))
        {
            TryFindFieldOrPropertyAssignmentStatement(siblingParam, blockStatement, out var sibling);
            if (sibling != null)
                return sibling;
        }
 
        return null;
    }
 
    private IFieldSymbol CreateField(
        IParameterSymbol parameter,
        AccessibilityModifiersRequired accessibilityModifiersRequired,
        ImmutableArray<NamingRule> rules)
    {
        var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;
 
        foreach (var rule in rules)
        {
            if (rule.SymbolSpecification.AppliesTo(SymbolKind.Field, Accessibility.Private))
            {
                var uniqueName = GenerateUniqueName(parameter, parameterNameParts, rule);
 
                var accessibilityLevel = Accessibility.Private;
                if (accessibilityModifiersRequired is AccessibilityModifiersRequired.Never or AccessibilityModifiersRequired.OmitIfDefault)
                {
                    var defaultAccessibility = DetermineDefaultFieldAccessibility(parameter.ContainingType);
                    if (defaultAccessibility == Accessibility.Private)
                    {
                        accessibilityLevel = Accessibility.NotApplicable;
                    }
                }
 
                return CodeGenerationSymbolFactory.CreateFieldSymbol(
                    default,
                    accessibilityLevel,
                    DeclarationModifiers.ReadOnly,
                    parameter.Type, uniqueName);
            }
        }
 
        // We place a special rule in s_builtInRules that matches all fields.  So we should 
        // always find a matching rule.
        throw ExceptionUtilities.Unreachable();
    }
 
    private IPropertySymbol CreateProperty(
        IParameterSymbol parameter,
        AccessibilityModifiersRequired accessibilityModifiersRequired,
        ImmutableArray<NamingRule> rules)
    {
        var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;
 
        foreach (var rule in rules)
        {
            if (rule.SymbolSpecification.AppliesTo(SymbolKind.Property, Accessibility.Public))
            {
                var uniqueName = GenerateUniqueName(parameter, parameterNameParts, rule);
 
                var accessibilityLevel = Accessibility.Public;
                if (accessibilityModifiersRequired is AccessibilityModifiersRequired.Never or AccessibilityModifiersRequired.OmitIfDefault)
                {
                    var defaultAccessibility = DetermineDefaultPropertyAccessibility();
                    if (defaultAccessibility == Accessibility.Public)
                    {
                        accessibilityLevel = Accessibility.NotApplicable;
                    }
                }
 
                var getMethod = CodeGenerationSymbolFactory.CreateAccessorSymbol(
                    default,
                    Accessibility.Public,
                    default);
 
                return CodeGenerationSymbolFactory.CreatePropertySymbol(
                    default,
                    accessibilityLevel,
                    new DeclarationModifiers(),
                    parameter.Type,
                    RefKind.None,
                    explicitInterfaceImplementations: default,
                    name: uniqueName,
                    parameters: default,
                    getMethod: getMethod,
                    setMethod: null);
            }
        }
 
        // We place a special rule in s_builtInRules that matches all properties.  So we should 
        // always find a matching rule.
        throw ExceptionUtilities.Unreachable();
    }
 
    private async Task<Solution> AddAllSymbolInitializationsAsync(
        Document document,
        SyntaxNode constructorDeclaration,
        IBlockOperation? blockStatement,
        ImmutableArray<IParameterSymbol> parameters,
        ImmutableArray<ISymbol> fieldsOrProperties,
        CancellationToken cancellationToken)
    {
        Debug.Assert(parameters.Length >= 2);
        Debug.Assert(fieldsOrProperties.Length > 0);
        Debug.Assert(parameters.Length == fieldsOrProperties.Length);
 
        // Process each param+field/prop in order.  Apply the pair to the document getting the updated document.
        // Then find all the current data in that updated document and move onto the next pair.
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var nodesToTrack = new List<SyntaxNode> { constructorDeclaration };
        if (blockStatement != null)
            nodesToTrack.Add(blockStatement.Syntax);
 
        var trackedRoot = root.TrackNodes(nodesToTrack);
        var currentSolution = document.WithSyntaxRoot(trackedRoot).Project.Solution;
 
        for (var i = 0; i < parameters.Length; i++)
        {
            var parameter = parameters[i];
            var fieldOrProperty = fieldsOrProperties[i];
 
            var currentDocument = currentSolution.GetRequiredDocument(document.Id);
            var currentSemanticModel = await currentDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var currentCompilation = currentSemanticModel.Compilation;
            var currentRoot = await currentDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
            var currentConstructorDeclaration = currentRoot.GetCurrentNode(constructorDeclaration);
            if (currentConstructorDeclaration == null)
                continue;
 
            IBlockOperation? currentBlockStatement = null;
            if (blockStatement != null)
            {
                currentBlockStatement = (IBlockOperation?)currentSemanticModel.GetOperation(currentRoot.GetCurrentNode(blockStatement.Syntax)!, cancellationToken);
                if (currentBlockStatement == null)
                    continue;
            }
 
            var currentParameter = (IParameterSymbol?)parameter.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol();
            if (currentParameter == null)
                continue;
 
            // fieldOrProperty is a new member.  So we don't have to track it to this edit we're making.
 
            currentSolution = await AddSingleSymbolInitializationAsync(
                currentDocument,
                currentConstructorDeclaration,
                currentBlockStatement,
                currentParameter,
                fieldOrProperty,
                isThrowNotImplementedProperty: false,
                cancellationToken).ConfigureAwait(false);
        }
 
        return currentSolution;
    }
 
    private async Task<Solution> AddSingleSymbolInitializationAsync(
        Document document,
        SyntaxNode constructorDeclaration,
        IBlockOperation? blockStatement,
        IParameterSymbol parameter,
        ISymbol fieldOrProperty,
        bool isThrowNotImplementedProperty,
        CancellationToken cancellationToken)
    {
        var services = document.Project.Solution.Services;
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var editor = new SyntaxEditor(root, services);
        var generator = editor.Generator;
        var options = await document.GetCodeGenerationOptionsAsync(cancellationToken).ConfigureAwait(false);
        var codeGenerator = document.GetRequiredLanguageService<ICodeGenerationService>();
 
        if (fieldOrProperty.ContainingType == null)
        {
            // We're generating a new field/property.  Place into the containing type,
            // ideally before/after a relevant existing member.
            // First, look for the right containing type (As a type may be partial). 
            // We want the type-block that this constructor is contained within.
            var typeDeclaration = constructorDeclaration.GetAncestor<TTypeDeclarationSyntax>()!;
 
            // Now add the field/property to this type.  Use the 'ReplaceNode+callback' form
            // so that nodes will be appropriate tracked and so we can then update the constructor
            // below even after we've replaced the whole type with a new type.
            //
            // Note: We'll pass the appropriate options so that the new field/property 
            // is appropriate placed before/after an existing field/property.  We'll try
            // to preserve the same order for fields/properties that we have for the constructor
            // parameters.
            editor.ReplaceNode(
                typeDeclaration,
                (currentTypeDecl, _) =>
                {
                    if (fieldOrProperty is IPropertySymbol property)
                    {
                        return codeGenerator.AddProperty(
                            currentTypeDecl, property,
                            codeGenerator.GetInfo(GetAddContext<IPropertySymbol>(parameter, blockStatement, typeDeclaration, cancellationToken), options, root.SyntaxTree.Options),
                            cancellationToken);
                    }
                    else if (fieldOrProperty is IFieldSymbol field)
                    {
                        return codeGenerator.AddField(
                            currentTypeDecl, field,
                            codeGenerator.GetInfo(GetAddContext<IFieldSymbol>(parameter, blockStatement, typeDeclaration, cancellationToken), options, root.SyntaxTree.Options),
                            cancellationToken);
                    }
                    else
                    {
                        throw ExceptionUtilities.Unreachable();
                    }
                });
        }
 
        AddAssignment(constructorDeclaration, blockStatement, parameter, fieldOrProperty, editor);
 
        // If the user had a property that has 'throw NotImplementedException' in it, then remove those throws.
        var currentSolution = document.Project.Solution;
        if (isThrowNotImplementedProperty)
        {
            var declarationService = document.GetRequiredLanguageService<ISymbolDeclarationService>();
            var propertySyntax = await declarationService.GetDeclarations(fieldOrProperty)[0].GetSyntaxAsync(cancellationToken).ConfigureAwait(false);
            var withoutThrowNotImplemented = RemoveThrowNotImplemented(propertySyntax);
 
            if (propertySyntax.SyntaxTree == root.SyntaxTree)
            {
                // Edit to the same file, just update this editor.
                editor.ReplaceNode(propertySyntax, withoutThrowNotImplemented);
            }
            else
            {
                // edit to a different file.  Just replace things directly in there.
                var otherDocument = currentSolution.GetDocument(propertySyntax.SyntaxTree);
                if (otherDocument != null)
                {
                    var otherRoot = await propertySyntax.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
                    currentSolution = currentSolution.WithDocumentSyntaxRoot(
                        otherDocument.Id, otherRoot.ReplaceNode(propertySyntax, withoutThrowNotImplemented));
                }
            }
        }
 
        return currentSolution.WithDocumentSyntaxRoot(document.Id, editor.GetChangedRoot());
    }
 
    private void AddAssignment(
        SyntaxNode constructorDeclaration,
        IBlockOperation? blockStatement,
        IParameterSymbol parameter,
        ISymbol fieldOrProperty,
        SyntaxEditor editor)
    {
        // First see if the user has `(_x, y) = (x, y);` and attempt to update that. 
        if (TryUpdateTupleAssignment(blockStatement, parameter, fieldOrProperty, editor))
            return;
 
        var generator = editor.Generator;
 
        // Now that we've added any potential members, create an assignment between it
        // and the parameter.
        var initializationStatement = (TStatementSyntax)generator.ExpressionStatement(
            generator.AssignmentStatement(
                generator.MemberAccessExpression(
                    generator.ThisExpression(),
                    generator.IdentifierName(fieldOrProperty.Name)),
                generator.IdentifierName(parameter.Name)));
 
        // Attempt to place the initialization in a good location in the constructor
        // We'll want to keep initialization statements in the same order as we see
        // parameters for the constructor.
        var statementToAddAfter = TryGetStatementToAddInitializationAfter(parameter, blockStatement);
 
        InsertStatement(editor, constructorDeclaration, returnsVoid: true, statementToAddAfter, initializationStatement);
    }
 
    private static CodeGenerationContext GetAddContext<TSymbol>(
        IParameterSymbol parameter, IBlockOperation? blockStatement,
        SyntaxNode typeDeclaration, CancellationToken cancellationToken)
        where TSymbol : ISymbol
    {
        foreach (var (sibling, before) in GetSiblingParameters(parameter))
        {
            var statement = TryFindFieldOrPropertyAssignmentStatement(
                sibling, blockStatement, out var fieldOrProperty);
 
            if (statement != null &&
                fieldOrProperty is TSymbol symbol)
            {
                var symbolSyntax = symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
                if (symbolSyntax.Ancestors().Contains(typeDeclaration))
                {
                    if (before)
                    {
                        // Found an existing field/property that corresponds to a preceding parameter.
                        // Place ourselves directly after it.
                        return new CodeGenerationContext(afterThisLocation: symbolSyntax.GetLocation());
                    }
                    else
                    {
                        // Found an existing field/property that corresponds to a following parameter.
                        // Place ourselves directly before it.
                        return new CodeGenerationContext(beforeThisLocation: symbolSyntax.GetLocation());
                    }
                }
            }
        }
 
        return CodeGenerationContext.Default;
    }
 
    private SyntaxNode? TryGetStatementToAddInitializationAfter(
        IParameterSymbol parameter, IBlockOperation? blockStatement)
    {
        // look for an existing assignment for a parameter that comes before/after us.
        // If we find one, we'll add ourselves before/after that parameter check.
        foreach (var (sibling, before) in GetSiblingParameters(parameter))
        {
            var statement = TryFindFieldOrPropertyAssignmentStatement(sibling, blockStatement);
            if (statement != null)
            {
                if (before)
                {
                    return statement.Syntax;
                }
                else
                {
                    var statementIndex = blockStatement!.Operations.IndexOf(statement);
                    return statementIndex > 0 && blockStatement.Operations[statementIndex - 1] is { IsImplicit: false, Syntax: var priorSyntax }
                        ? priorSyntax
                        : null;
                }
            }
        }
 
        // We couldn't find a reasonable location for the new initialization statement.
        // Just place ourselves after the last statement in the constructor.
        return TryGetLastStatement(blockStatement);
    }
 
    private static IOperation? TryFindFieldOrPropertyAssignmentStatement(IParameterSymbol parameter, IBlockOperation? blockStatement)
        => TryFindFieldOrPropertyAssignmentStatement(parameter, blockStatement, out _);
 
    protected static bool TryGetPartsOfTupleAssignmentOperation(
        IOperation operation,
        [NotNullWhen(true)] out ITupleOperation? targetTuple,
        [NotNullWhen(true)] out ITupleOperation? valueTuple)
    {
        if (operation is IExpressionStatementOperation
            {
                Operation: IDeconstructionAssignmentOperation
                {
                    Target: ITupleOperation targetTupleTemp,
                    Value: IConversionOperation { Operand: ITupleOperation valueTupleTemp },
                }
            } &&
            targetTupleTemp.Elements.Length == valueTupleTemp.Elements.Length)
        {
            targetTuple = targetTupleTemp;
            valueTuple = valueTupleTemp;
            return true;
        }
 
        targetTuple = null;
        valueTuple = null;
        return false;
    }
 
    private static IOperation? TryFindFieldOrPropertyAssignmentStatement(
        IParameterSymbol parameter, IBlockOperation? blockStatement, out ISymbol? fieldOrProperty)
    {
        if (blockStatement != null)
        {
            var containingType = parameter.ContainingType;
            foreach (var statement in blockStatement.Operations)
            {
                // look for something of the form:  "this.s = s" or "this.s = s ?? ..."
                if (IsFieldOrPropertyAssignment(statement, containingType, out var assignmentExpression, out fieldOrProperty) &&
                    IsParameterReferenceOrCoalesceOfParameterReference(assignmentExpression, parameter))
                {
                    return statement;
                }
 
                // look inside the form `(this.s, this.t) = (s, t)`
                if (TryGetPartsOfTupleAssignmentOperation(statement, out var targetTuple, out var valueTuple))
                {
                    for (int i = 0, n = targetTuple.Elements.Length; i < n; i++)
                    {
                        var target = targetTuple.Elements[i];
                        var value = valueTuple.Elements[i];
 
                        if (IsFieldOrPropertyReference(target, containingType, out fieldOrProperty) &&
                            IsParameterReference(value, parameter))
                        {
                            return statement;
                        }
                    }
                }
            }
        }
 
        fieldOrProperty = null;
        return null;
    }
 
    private static bool IsParameterReferenceOrCoalesceOfParameterReference(
        IAssignmentOperation assignmentExpression, IParameterSymbol parameter)
        => InitializeParameterHelpersCore.IsParameterReferenceOrCoalesceOfParameterReference(assignmentExpression.Value, parameter);
 
    private async Task<(ISymbol?, bool isThrowNotImplementedProperty)> TryFindMatchingUninitializedFieldOrPropertySymbolAsync(
        Document document, IParameterSymbol parameter, IBlockOperation? blockStatement, ImmutableArray<NamingRule> rules, ImmutableArray<string> parameterWords, CancellationToken cancellationToken)
    {
        // Look for a field/property that really looks like it corresponds to this parameter.
        // Use a variety of heuristics around the name/type to see if this is a match.
 
        var containingType = parameter.ContainingType;
        var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
 
        // Walk through the naming rules against this parameter's name to see what
        // name the user would like for it as a member in this type.  Note that we
        // have some fallback rules that use the standard conventions around 
        // properties /fields so that can still find things even if the user has no
        // naming preferences set.
 
        foreach (var rule in rules)
        {
            var memberName = rule.NamingStyle.CreateName(parameterWords);
            foreach (var memberWithName in containingType.GetMembers(memberName))
            {
                // We found members in our type with that name.  If it's a writable
                // field that we could assign this parameter to, and it's not already
                // been assigned to, then this field is a good candidate for us to
                // hook up to.
                if (memberWithName is IFieldSymbol field &&
                    !field.IsConst &&
                    IsImplicitConversion(compilation, source: parameter.Type, destination: field.Type) &&
                    !ContainsMemberAssignment(blockStatement, field))
                {
                    return (field, isThrowNotImplementedProperty: false);
                }
 
                // If it's a writable property that we could assign this parameter to, and it's
                // not already been assigned to, then this property is a good candidate for us to
                // hook up to.
                if (memberWithName is IPropertySymbol property &&
                    IsImplicitConversion(compilation, source: parameter.Type, destination: property.Type) &&
                    !ContainsMemberAssignment(blockStatement, property))
                {
                    // We also allow assigning into a property of the form `=> throw new NotImplementedException()`.
                    // That way users can easily spit out those methods, but then convert them to be normal
                    // properties with ease.
                    if (IsThrowNotImplementedProperty(property))
                        return (property, isThrowNotImplementedProperty: true);
 
                    if (property.IsWritableInConstructor())
                        return (property, isThrowNotImplementedProperty: false);
                }
            }
        }
 
        // Couldn't find any existing member.  Just return nothing so we can offer to
        // create a member for them.
        return default;
 
        static bool ContainsMemberAssignment(IBlockOperation? blockStatement, ISymbol member)
        {
            if (blockStatement != null)
            {
                foreach (var statement in blockStatement.Operations)
                {
                    if (IsFieldOrPropertyAssignment(statement, member.ContainingType, out var assignmentExpression) &&
                        assignmentExpression.Target.UnwrapImplicitConversion() is IMemberReferenceOperation memberReference &&
                        member.Equals(memberReference.Member))
                    {
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        bool IsThrowNotImplementedProperty(IPropertySymbol property)
        {
            using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var accessors);
 
            if (property.GetMethod != null)
                accessors.AddIfNotNull(GetAccessorBody(property.GetMethod, cancellationToken));
 
            if (property.SetMethod != null)
                accessors.AddIfNotNull(GetAccessorBody(property.SetMethod, cancellationToken));
 
            if (accessors.Count == 0)
                return false;
 
            foreach (var group in accessors.GroupBy(node => node.SyntaxTree))
            {
                var semanticModel = compilation.GetSemanticModel(group.Key);
                foreach (var accessorBody in accessors)
                {
                    var operation = semanticModel.GetOperation(accessorBody, cancellationToken);
                    if (operation is null)
                        return false;
 
                    if (!operation.IsSingleThrowNotImplementedOperation())
                        return false;
                }
            }
 
            return true;
        }
    }
}