File: InitializeParameter\CSharpInitializeMemberFromPrimaryConstructorParameterCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.InitializeParameter;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Naming;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter;
 
using static InitializeParameterHelpers;
using static InitializeParameterHelpersCore;
using static SyntaxFactory;
 
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InitializeMemberFromPrimaryConstructorParameter), Shared]
internal sealed partial class CSharpInitializeMemberFromPrimaryConstructorParameterCodeRefactoringProvider
    : CodeRefactoringProvider
{
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public CSharpInitializeMemberFromPrimaryConstructorParameterCodeRefactoringProvider()
    {
    }
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, _, cancellationToken) = context;
 
        var selectedParameter = await context.TryGetRelevantNodeAsync<ParameterSyntax>().ConfigureAwait(false);
        if (selectedParameter == null)
            return;
 
        if (selectedParameter.Parent is not ParameterListSyntax { Parent: TypeDeclarationSyntax(kind: SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration) typeDeclaration })
            return;
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var parameter = semanticModel.GetRequiredDeclaredSymbol(selectedParameter, cancellationToken);
        if (parameter?.Name is null or "")
            return;
 
        if (parameter.ContainingSymbol is not IMethodSymbol { MethodKind: MethodKind.Constructor } constructor)
            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 compilation = semanticModel.Compilation;
        var (initializerValue, _) = TryFindFieldOrPropertyInitializerValue(compilation, parameter, cancellationToken);
        if (initializerValue != 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 formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        var fieldOrProperty = TryFindMatchingUninitializedFieldOrPropertySymbol();
        var refactorings = fieldOrProperty == null
            ? HandleNoExistingFieldOrProperty()
            : HandleExistingFieldOrProperty();
 
        context.RegisterRefactorings(refactorings.ToImmutableArray(), context.Span);
        return;
 
        ISymbol? TryFindMatchingUninitializedFieldOrPropertySymbol()
        {
            // 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 parameterWords = parameterNameParts.BaseNameParts;
            var containingType = parameter.ContainingType;
 
            // 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 { IsConst: false, DeclaringSyntaxReferences: [var syntaxRef1, ..] } field &&
                        IsImplicitConversion(compilation, source: parameter.Type, destination: field.Type) &&
                        syntaxRef1.GetSyntax(cancellationToken) is VariableDeclaratorSyntax { Initializer: null })
                    {
                        return field;
                    }
 
                    // 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 { DeclaringSyntaxReferences: [var syntaxRef2, ..] } property &&
                        IsImplicitConversion(compilation, source: parameter.Type, destination: property.Type) &&
                        syntaxRef2.GetSyntax(cancellationToken) is PropertyDeclarationSyntax { Initializer: null })
                    {
                        // 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(compilation, property, cancellationToken))
                            return property;
 
                        if (property.IsWritableInConstructor())
                            return property;
                    }
                }
            }
 
            // Couldn't find any existing member.  Just return nothing so we can offer to create a member for them.
            return null;
        }
 
        static CodeAction CreateCodeAction(string title, Func<CancellationToken, Task<Solution>> createSolution)
            => CodeAction.Create(title, createSolution, title);
 
        IEnumerable<CodeAction> HandleExistingFieldOrProperty()
        {
            // Found a field/property that this parameter should be assigned to. Just offer the simple assignment to it.
            yield return CreateCodeAction(
                string.Format(fieldOrProperty.Kind == SymbolKind.Field ? FeaturesResources.Initialize_field_0 : FeaturesResources.Initialize_property_0, fieldOrProperty.Name),
                cancellationToken => UpdateExistingMemberAsync(document, parameter, fieldOrProperty, cancellationToken));
        }
 
        IEnumerable<CodeAction> HandleNoExistingFieldOrProperty()
        {
            // Didn't find a field/prop that this parameter could be assigned to. Offer to create new one and assign to that.
 
            // 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();
 
            var field = CreateField(parameter);
            var property = CreateProperty(parameter);
 
            var fieldAction = CreateCodeAction(
                string.Format(FeaturesResources.Create_and_assign_field_0, field.Name),
                cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, [parameter], [field], cancellationToken));
            var propertyAction = CreateCodeAction(
                string.Format(FeaturesResources.Create_and_assign_property_0, property.Name),
                cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, [parameter], [property], cancellationToken));
 
            yield return siblingFieldOrProperty is IFieldSymbol ? fieldAction : propertyAction;
            yield return siblingFieldOrProperty is IFieldSymbol ? propertyAction : fieldAction;
 
            var parameters = GetParametersWithoutAssociatedMembers();
            if (parameters.Length >= 2)
            {
                var allFieldsAction = CodeAction.Create(
                    FeaturesResources.Create_and_assign_remaining_as_fields,
                    cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, parameters, parameters.SelectAsArray(CreateField), cancellationToken));
                var allPropertiesAction = CodeAction.Create(
                    FeaturesResources.Create_and_assign_remaining_as_properties,
                    cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, parameters, parameters.SelectAsArray(CreateProperty), cancellationToken));
 
                yield return siblingFieldOrProperty is IFieldSymbol ? allFieldsAction : allPropertiesAction;
                yield return siblingFieldOrProperty is IFieldSymbol ? allPropertiesAction : allFieldsAction;
            }
        }
 
        ISymbol? TryFindSiblingFieldOrProperty()
        {
            foreach (var (siblingParam, _) in InitializeParameterHelpersCore.GetSiblingParameters(parameter))
            {
                var (_, sibling) = TryFindFieldOrPropertyInitializerValue(compilation, siblingParam, cancellationToken);
                if (sibling != null)
                    return sibling;
            }
 
            return null;
        }
 
        ImmutableArray<IParameterSymbol> GetParametersWithoutAssociatedMembers()
        {
            using var result = TemporaryArray<IParameterSymbol>.Empty;
 
            foreach (var parameter in constructor.Parameters)
            {
                var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules);
                if (parameterNameParts.BaseName == "")
                    continue;
 
                var (assignmentOp, _) = TryFindFieldOrPropertyInitializerValue(compilation, parameter, cancellationToken);
                if (assignmentOp != null)
                    continue;
 
                result.Add(parameter);
            }
 
            return result.ToImmutableAndClear();
        }
 
        ISymbol CreateField(IParameterSymbol parameter)
        {
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;
            var accessibilityLevel = formattingOptions.AccessibilityModifiersRequired is AccessibilityModifiersRequired.Never or AccessibilityModifiersRequired.OmitIfDefault
                ? Accessibility.NotApplicable
                : Accessibility.Private;
 
            foreach (var rule in rules)
            {
                if (rule.SymbolSpecification.AppliesTo(SymbolKind.Field, Accessibility.Private))
                {
                    return CodeGenerationSymbolFactory.CreateFieldSymbol(
                        attributes: default,
                        accessibilityLevel,
                        DeclarationModifiers.ReadOnly,
                        parameter.Type,
                        name: GenerateUniqueName(parameter, parameterNameParts, rule),
                        initializer: IdentifierName(parameter.Name.EscapeIdentifier()));
                }
            }
 
            // We place a special rule in s_builtInRules that matches all fields.  So we should 
            // always find a matching rule.
            throw ExceptionUtilities.Unreachable();
        }
 
        ISymbol CreateProperty(IParameterSymbol parameter)
        {
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts;
 
            foreach (var rule in rules)
            {
                if (rule.SymbolSpecification.AppliesTo(SymbolKind.Property, Accessibility.Public))
                {
                    return CodeGenerationSymbolFactory.CreatePropertySymbol(
                        containingType: null,
                        attributes: default,
                        Accessibility.Public,
                        modifiers: default,
                        parameter.Type,
                        RefKind.None,
                        explicitInterfaceImplementations: default,
                        name: GenerateUniqueName(parameter, parameterNameParts, rule),
                        parameters: default,
                        getMethod: CodeGenerationSymbolFactory.CreateAccessorSymbol(
                            attributes: default,
                            Accessibility.Public,
                            statements: default),
                        setMethod: null,
                        initializer: IdentifierName(parameter.Name.EscapeIdentifier()));
                }
            }
 
            // We place a special rule in s_builtInRules that matches all properties.  So we should 
            // always find a matching rule.
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    private static (IOperation? initializer, ISymbol? fieldOrProperty) TryFindFieldOrPropertyInitializerValue(
        Compilation compilation,
        IParameterSymbol parameter,
        CancellationToken cancellationToken)
    {
        foreach (var group in parameter.ContainingType.DeclaringSyntaxReferences.GroupBy(r => r.SyntaxTree))
        {
            var semanticModel = compilation.GetSemanticModel(group.Key);
            foreach (var syntaxReference in group)
            {
                if (syntaxReference.GetSyntax(cancellationToken) is TypeDeclarationSyntax typeDeclaration)
                {
                    foreach (var member in typeDeclaration.Members)
                    {
                        if (member is PropertyDeclarationSyntax { Initializer.Value: var propertyInitializer } propertyDeclaration)
                        {
                            var operation = semanticModel.GetOperation(propertyInitializer, cancellationToken);
                            if (IsParameterReferenceOrCoalesceOfParameterReference(operation, parameter))
                                return (operation, semanticModel.GetRequiredDeclaredSymbol(propertyDeclaration, cancellationToken));
                        }
                        else if (member is FieldDeclarationSyntax field)
                        {
                            foreach (var varDecl in field.Declaration.Variables)
                            {
                                if (varDecl is { Initializer.Value: var fieldInitializer })
                                {
                                    var operation = semanticModel.GetOperation(fieldInitializer, cancellationToken);
                                    if (IsParameterReferenceOrCoalesceOfParameterReference(operation, parameter))
                                        return (operation, semanticModel.GetRequiredDeclaredSymbol(varDecl, cancellationToken));
                                }
                            }
                        }
                    }
                }
            }
        }
 
        return default;
    }
}