File: src\Analyzers\Core\CodeFixes\GenerateConstructor\GenerateConstructorHelpers.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.InitializeParameter;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Naming;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.GenerateMember.GenerateConstructor;
 
internal static class GenerateConstructorHelpers
{
    public static bool CanDelegateTo<TExpressionSyntax>(
        SemanticDocument document,
        ImmutableArray<IParameterSymbol> parameters,
        ImmutableArray<TExpressionSyntax?> expressions,
        IMethodSymbol constructor)
        where TExpressionSyntax : SyntaxNode
    {
        // Look for constructors in this specified type that are:
        // 1. Accessible.  We obviously need our constructor to be able to call that other constructor.
        // 2. Won't cause a cycle.  i.e. if we're generating a new constructor from an existing constructor,
        //    then we don't want it calling back into us.
        // 3. Are compatible with the parameters we're generating for this constructor.  Compatible means there
        //    exists an implicit conversion from the new constructor's parameter types to the existing
        //    constructor's parameter types.
        var semanticFacts = document.Document.GetRequiredLanguageService<ISemanticFactsService>();
        var semanticModel = document.SemanticModel;
        var compilation = semanticModel.Compilation;
 
        return constructor.Parameters.Length == parameters.Length &&
               constructor.Parameters.SequenceEqual(parameters, (p1, p2) => p1.RefKind == p2.RefKind) &&
               IsSymbolAccessible(compilation, constructor) &&
               IsCompatible(semanticFacts, semanticModel, constructor, expressions);
    }
 
    private static bool IsSymbolAccessible(Compilation compilation, ISymbol symbol)
    {
        if (symbol == null)
            return false;
 
        if (symbol is IPropertySymbol { SetMethod: { } setMethod } &&
            !IsSymbolAccessible(compilation, setMethod))
        {
            return false;
        }
 
        // Public and protected constructors are accessible.  Internal constructors are
        // accessible if we have friend access.  We can't call the normal accessibility
        // checkers since they will think that a protected constructor isn't accessible
        // (since we don't have the destination type that would have access to them yet).
        switch (symbol.DeclaredAccessibility)
        {
            case Accessibility.ProtectedOrInternal:
            case Accessibility.Protected:
            case Accessibility.Public:
                return true;
            case Accessibility.ProtectedAndInternal:
            case Accessibility.Internal:
                return compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(symbol.ContainingAssembly);
 
            default:
                return false;
        }
    }
 
    private static bool IsCompatible<TExpressionSyntax>(
        ISemanticFactsService semanticFacts,
        SemanticModel semanticModel,
        IMethodSymbol constructor,
        ImmutableArray<TExpressionSyntax?> expressions)
        where TExpressionSyntax : SyntaxNode
    {
        Debug.Assert(constructor.Parameters.Length == expressions.Length);
 
        // Resolve the constructor into our semantic model's compilation; if the constructor we're looking at is from
        // another project with a different language.
        var constructorInCompilation = (IMethodSymbol?)SymbolKey.Create(constructor).Resolve(semanticModel.Compilation).Symbol;
 
        if (constructorInCompilation == null)
        {
            // If the constructor can't be mapped into our invocation project, we'll just bail.
            // Note the logic in this method doesn't handle a complicated case where:
            //
            // 1. Project A has some public type.
            // 2. Project B references A, and has one constructor that uses the public type from A.
            // 3. Project C, which references B but not A, has an invocation of B's constructor passing some
            //    parameters.
            //
            // The algorithm of this class tries to map the constructor in B (that we might delegate to) into
            // C, but that constructor might not be mappable if the public type from A is not available.
            // However, theoretically the public type from A could have a user-defined conversion.
            // The alternative approach might be to map the type of the parameters back into B, and then
            // classify the conversions in Project B, but that'll run into other issues if the experssions
            // don't have a natural type (like default). We choose to ignore all complicated cases here.
            return false;
        }
 
        for (var i = 0; i < constructorInCompilation.Parameters.Length; i++)
        {
            var constructorParameter = constructorInCompilation.Parameters[i];
            if (constructorParameter == null)
                return false;
 
            var expression = expressions[i];
            if (expression is null)
                continue;
 
            var conversion = semanticFacts.ClassifyConversion(semanticModel, expression, constructorParameter.Type);
            if (!conversion.IsIdentity && !conversion.IsImplicit)
                return false;
        }
 
        return true;
    }
 
    public static async Task<
            (ImmutableArray<IParameterSymbol> parameters,
             ImmutableDictionary<string, ISymbol> parameterToExistingMemberMap,
             ImmutableDictionary<string, string> parameterToNewFieldMap,
             ImmutableDictionary<string, string> parameterToNewPropertyMap)> GetParametersAsync<TExpressionSyntax>(
        SemanticDocument document,
        INamedTypeSymbol typeToGenerateIn,
        ImmutableArray<Argument<TExpressionSyntax>> arguments,
        ImmutableArray<ITypeSymbol> parameterTypes,
        ImmutableArray<ParameterName> parameterNames,
        CancellationToken cancellationToken)
        where TExpressionSyntax : SyntaxNode
    {
        var fieldNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Field, Accessibility.Private, cancellationToken).ConfigureAwait(false);
        var propertyNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Property, Accessibility.Public, cancellationToken).ConfigureAwait(false);
        var parameterNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Parameter, Accessibility.NotApplicable, cancellationToken).ConfigureAwait(false);
 
        var rules = await document.Document.GetNamingRulesAsync(cancellationToken).ConfigureAwait(false);
 
        var parameterToExistingMemberMap = ImmutableDictionary.CreateBuilder<string, ISymbol>();
        var parameterToNewFieldMap = ImmutableDictionary.CreateBuilder<string, string>();
        var parameterToNewPropertyMap = ImmutableDictionary.CreateBuilder<string, string>();
 
        var unavailableMemberNames = GetUnavailableMemberNames(typeToGenerateIn).ToImmutableArray();
 
        using var _ = ArrayBuilder<IParameterSymbol>.GetInstance(out var parameters);
 
        for (var i = 0; i < parameterNames.Length; i++)
        {
            var parameterName = parameterNames[i];
            var parameterType = parameterTypes[i];
            var argument = arguments[i];
 
            // See if there's a matching field or property we can use, or create a new member otherwise.
            parameterName = FindExistingOrCreateNewMember(parameterName, parameterType, argument);
 
            parameters.Add(CodeGenerationSymbolFactory.CreateParameterSymbol(
                attributes: default,
                refKind: argument.RefKind,
                isParams: false,
                type: parameterType,
                name: parameterName.BestNameForParameter));
        }
 
        return (parameters.ToImmutable(),
            parameterToExistingMemberMap.ToImmutable(),
            parameterToNewFieldMap.ToImmutable(),
            parameterToNewPropertyMap.ToImmutable());
 
        ParameterName FindExistingOrCreateNewMember(
            ParameterName parameterName,
            ITypeSymbol parameterType,
            Argument<TExpressionSyntax> argument)
        {
            var isFixed = argument.IsNamed;
 
            var symbol = TryFindMatchingMember(parameterName);
            if (symbol != null)
            {
                if (IsViableFieldOrProperty(document, parameterType, symbol, cancellationToken))
                {
                    // Ok!  We can just the existing field.  
                    parameterToExistingMemberMap[parameterName.BestNameForParameter] = symbol;
                }
                else
                {
                    // Uh-oh.  Now we have a problem.  We can't assign this parameter to
                    // this field.  So we need to create a new field.  Find a name not in
                    // use so we can assign to that.  
                    var baseName = GenerateNameForArgument(document, argument, cancellationToken);
 
                    var baseFieldWithNamingStyle = fieldNamingRule.NamingStyle.MakeCompliant(baseName).First();
                    var basePropertyWithNamingStyle = propertyNamingRule.NamingStyle.MakeCompliant(baseName).First();
 
                    var newFieldName = NameGenerator.EnsureUniqueness(baseFieldWithNamingStyle, unavailableMemberNames.Concat(parameterToNewFieldMap.Values));
                    var newPropertyName = NameGenerator.EnsureUniqueness(basePropertyWithNamingStyle, unavailableMemberNames.Concat(parameterToNewPropertyMap.Values));
 
                    if (isFixed)
                    {
                        // Can't change the parameter name, so map the existing parameter
                        // name to the new field name.
                        parameterToNewFieldMap[parameterName.NameBasedOnArgument] = newFieldName;
                        parameterToNewPropertyMap[parameterName.NameBasedOnArgument] = newPropertyName;
                    }
                    else
                    {
                        // Can change the parameter name, so do so.  
                        // But first remove any prefix added due to field naming styles
                        var fieldNameMinusPrefix = newFieldName[fieldNamingRule.NamingStyle.Prefix.Length..];
                        var newParameterName = new ParameterName(fieldNameMinusPrefix, isFixed: false, parameterNamingRule);
                        parameterName = newParameterName;
 
                        parameterToNewFieldMap[newParameterName.BestNameForParameter] = newFieldName;
                        parameterToNewPropertyMap[newParameterName.BestNameForParameter] = newPropertyName;
                    }
                }
            }
            else
            {
                // If no matching field was found, use the fieldNamingRule to create suitable name
                var bestNameForParameter = parameterName.BestNameForParameter;
                var nameBasedOnArgument = parameterName.NameBasedOnArgument;
                parameterToNewFieldMap[bestNameForParameter] = fieldNamingRule.NamingStyle.MakeCompliant(nameBasedOnArgument).First();
                parameterToNewPropertyMap[bestNameForParameter] = propertyNamingRule.NamingStyle.MakeCompliant(nameBasedOnArgument).First();
            }
 
            return parameterName;
        }
 
        ISymbol? TryFindMatchingMember(ParameterName parameterName)
        {
            var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameterName.NameBasedOnArgument);
 
            // Try with the explicit field naming rule first, so that we def match with a field that matches the user's
            // chosen naming settings.
            var symbol = TryFindMemberWithRule(fieldNamingRule, parameterNameParts);
            if (symbol != null)
                return symbol;
 
            // But if that fails, fall back to other common ways of naming fields (for example, with or without an
            // underscore prefix), to see if we can find a match that way.
            foreach (var rule in rules)
            {
                symbol = TryFindMemberWithRule(rule, parameterNameParts);
                if (symbol != null)
                    return symbol;
            }
 
            return null;
        }
 
        ISymbol? TryFindMemberWithRule(NamingRule rule, IdentifierNameParts parameterNameParts)
        {
            var memberName = rule.NamingStyle.CreateName(parameterNameParts.BaseNameParts);
 
            // For non-out parameters, see if there's already a field there with the same name.
            // If so, and it has a compatible type, then we can just assign to that field.
            // Otherwise, we'll need to choose a different name for this member so that it
            // doesn't conflict with something already in the type. First check the current type
            // for a matching field.  If so, defer to it.
 
            var members = from t in typeToGenerateIn.GetBaseTypesAndThis()
                          let ignoreAccessibility = t.Equals(typeToGenerateIn)
                          from m in t.GetMembers()
                          where m.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase)
                          where ignoreAccessibility || IsSymbolAccessible(m, document)
                          select m;
 
            var membersArray = members.ToImmutableArray();
 
            return membersArray.FirstOrDefault(m => m.Name.Equals(memberName, StringComparison.Ordinal)) ?? membersArray.FirstOrDefault();
        }
    }
 
    private static string GenerateNameForArgument<TExpressionSyntax>(
        SemanticDocument document, Argument<TExpressionSyntax> argument, CancellationToken cancellationToken)
        where TExpressionSyntax : SyntaxNode
    {
        // If it named argument then we use the name provided.
        if (argument.IsNamed)
            return argument.Name;
 
        if (argument.Expression is null)
            return ITypeSymbolExtensions.DefaultParameterName;
 
        var semanticFacts = document.Document.GetRequiredLanguageService<ISemanticFactsService>();
        var name = semanticFacts.GenerateNameForExpression(
            document.SemanticModel, argument.Expression, capitalize: false, cancellationToken);
        return string.IsNullOrEmpty(name) ? ITypeSymbolExtensions.DefaultParameterName : name;
    }
 
    private static IEnumerable<string> GetUnavailableMemberNames(INamedTypeSymbol typeToGenerateIn)
    {
        Contract.ThrowIfNull(typeToGenerateIn);
 
        return typeToGenerateIn.MemberNames.Concat(
            from type in typeToGenerateIn.GetBaseTypes()
            from member in type.GetMembers()
            select member.Name);
    }
 
    private static bool IsSymbolAccessible(ISymbol? symbol, SemanticDocument document)
    {
        if (symbol == null)
        {
            return false;
        }
 
        if (symbol.Kind == SymbolKind.Property)
        {
            if (!IsSymbolAccessible(((IPropertySymbol)symbol).SetMethod, document))
            {
                return false;
            }
        }
 
        // Public and protected constructors are accessible.  Internal constructors are
        // accessible if we have friend access.  We can't call the normal accessibility
        // checkers since they will think that a protected constructor isn't accessible
        // (since we don't have the destination type that would have access to them yet).
        switch (symbol.DeclaredAccessibility)
        {
            case Accessibility.ProtectedOrInternal:
            case Accessibility.Protected:
            case Accessibility.Public:
                return true;
            case Accessibility.ProtectedAndInternal:
            case Accessibility.Internal:
                return document.SemanticModel.Compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(
                    symbol.ContainingAssembly);
 
            default:
                return false;
        }
    }
 
    private static bool IsViableFieldOrProperty(
        SemanticDocument document,
        ITypeSymbol parameterType,
        ISymbol symbol,
        CancellationToken cancellationToken)
    {
        if (parameterType.Language != symbol.Language)
            return false;
 
        if (symbol != null && !symbol.IsStatic)
        {
            if (symbol is IFieldSymbol { IsConst: false } field)
            {
                return IsConversionImplicit(document.SemanticModel.Compilation, parameterType, field.Type);
            }
            else if (symbol is IPropertySymbol { Parameters.Length: 0 } property)
            {
                if (!IsConversionImplicit(document.SemanticModel.Compilation, parameterType, property.Type))
                    return false;
 
                if (property.IsWritableInConstructor())
                    return true;
 
                var service = document.Document.GetRequiredLanguageService<IInitializeParameterService>();
                return service.IsThrowNotImplementedProperty(document.SemanticModel.Compilation, property, cancellationToken);
            }
        }
 
        return false;
    }
 
    private static bool IsConversionImplicit(Compilation compilation, ITypeSymbol sourceType, ITypeSymbol targetType)
        => compilation.ClassifyCommonConversion(sourceType, targetType).IsImplicit;
}