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.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
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;
    }
}