File: GenerateType\AbstractGenerateTypeService.State.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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.GenerateType;
 
internal abstract partial class AbstractGenerateTypeService<TService, TSimpleNameSyntax, TObjectCreationExpressionSyntax, TExpressionSyntax, TTypeDeclarationSyntax, TArgumentSyntax>
{
    protected class State
    {
        public string Name { get; private set; } = null!;
        public bool NameIsVerbatim { get; private set; }
 
        // The name node that we're on.  Will be used to the name the type if it's
        // generated.
        public TSimpleNameSyntax SimpleName { get; private set; } = null!;
 
        // The entire expression containing the name, not including the creation.  i.e. "X.Goo"
        // in "new X.Goo()".
        public TExpressionSyntax NameOrMemberAccessExpression { get; private set; } = null!;
 
        // The object creation node if we have one.  i.e. if we're on the 'Goo' in "new X.Goo()".
        public TObjectCreationExpressionSyntax? ObjectCreationExpressionOpt { get; private set; }
 
        // One of these will be non null.  It's also possible for both to be non null. For
        // example, if you have "class C { Goo f; }", then "Goo" can be generated inside C or
        // inside the global namespace.  The namespace can be null or the type can be null if the
        // user has something like "ExistingType.NewType" or "ExistingNamespace.NewType".  In
        // that case they're being explicit about what they want to generate into.
        public INamedTypeSymbol? TypeToGenerateInOpt { get; private set; }
        public string? NamespaceToGenerateInOpt { get; private set; }
 
        // If we can infer a base type or interface for this type. 
        // 
        // i.e.: "IList<int> goo = new MyList();"
        public INamedTypeSymbol? BaseTypeOrInterfaceOpt { get; private set; }
        public bool IsInterface { get; private set; }
        public bool IsStruct { get; private set; }
        public bool IsAttribute { get; private set; }
        public bool IsException { get; private set; }
        public bool IsMembersWithModule { get; private set; }
        public bool IsTypeGeneratedIntoNamespaceFromMemberAccess { get; private set; }
        public bool IsSimpleNameGeneric { get; private set; }
        public bool IsPublicAccessibilityForTypeGeneration { get; private set; }
        public bool IsInterfaceOrEnumNotAllowedInTypeContext { get; private set; }
        public IMethodSymbol? DelegateMethodSymbol { get; private set; }
        public bool IsDelegateAllowed { get; private set; }
        public bool IsEnumNotAllowed { get; private set; }
        public Compilation Compilation { get; }
        public bool IsDelegateOnly { get; private set; }
        public bool IsClassInterfaceTypes { get; private set; }
        public List<TSimpleNameSyntax> PropertiesToGenerate { get; private set; } = null!;
 
        private State(Compilation compilation)
            => Compilation = compilation;
 
        public static async Task<State?> GenerateAsync(
            TService service,
            SemanticDocument document,
            SyntaxNode node,
            CancellationToken cancellationToken)
        {
            var state = new State(document.SemanticModel.Compilation);
            if (!await state.TryInitializeAsync(service, document, node, cancellationToken).ConfigureAwait(false))
            {
                return null;
            }
 
            return state;
        }
 
        private async Task<bool> TryInitializeAsync(
            TService service,
            SemanticDocument semanticDocument,
            SyntaxNode node,
            CancellationToken cancellationToken)
        {
            if (node is not TSimpleNameSyntax)
            {
                return false;
            }
 
            SimpleName = (TSimpleNameSyntax)node;
            var syntaxFacts = semanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
            syntaxFacts.GetNameAndArityOfSimpleName(SimpleName, out var name, out _);
 
            Name = name;
            NameIsVerbatim = syntaxFacts.IsVerbatimIdentifier(SimpleName.GetFirstToken());
            if (string.IsNullOrWhiteSpace(Name))
            {
                return false;
            }
            // We only support simple names or dotted names.  i.e. "(some + expr).Goo" is not a
            // valid place to generate a type for Goo.
            if (!service.TryInitializeState(semanticDocument, SimpleName, cancellationToken, out var generateTypeServiceStateOptions))
            {
                return false;
            }
 
            if (char.IsLower(name[0]) && !semanticDocument.SemanticModel.Compilation.IsCaseSensitive)
            {
                // It's near universal in .NET that types start with a capital letter.  As such,
                // if this name starts with a lowercase letter, don't even bother to offer 
                // "generate type".  The user most likely wants to run 'Add Import' (which will
                // then fix up a case where they typed an existing type name in lowercase, 
                // intending the fix to case correct it).
                return false;
            }
 
            Contract.ThrowIfNull(generateTypeServiceStateOptions.NameOrMemberAccessExpression);
            NameOrMemberAccessExpression = generateTypeServiceStateOptions.NameOrMemberAccessExpression;
            ObjectCreationExpressionOpt = generateTypeServiceStateOptions.ObjectCreationExpressionOpt;
 
            var semanticModel = semanticDocument.SemanticModel;
            var info = semanticModel.GetSymbolInfo(SimpleName, cancellationToken);
            if (info.Symbol != null)
            {
                // This bound, so no need to generate anything.
                return false;
            }
 
            var semanticFacts = semanticDocument.Document.GetRequiredLanguageService<ISemanticFactsService>();
            if (!semanticFacts.IsTypeContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) &&
                !semanticFacts.IsExpressionContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) &&
                !semanticFacts.IsStatementContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) &&
                !semanticFacts.IsInsideNameOfExpression(semanticModel, NameOrMemberAccessExpression, cancellationToken) &&
                !semanticFacts.IsNamespaceContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken))
            {
                return false;
            }
 
            // If this isn't something that can be created, then don't bother offering to create
            // it.
            if (info.CandidateReason == CandidateReason.NotCreatable)
            {
                return false;
            }
 
            if (info.CandidateReason is CandidateReason.Inaccessible or
                CandidateReason.NotReferencable or
                CandidateReason.OverloadResolutionFailure)
            {
                // We bound to something inaccessible, or overload resolution on a 
                // constructor call failed.  Don't want to offer GenerateType here.
                return false;
            }
 
            if (ObjectCreationExpressionOpt != null)
            {
                // If we're new'ing up something illegal, then don't offer generate type.
                var typeInfo = semanticModel.GetTypeInfo(ObjectCreationExpressionOpt, cancellationToken);
                if (typeInfo.Type.IsModuleType())
                {
                    return false;
                }
            }
 
            await DetermineNamespaceOrTypeToGenerateInAsync(service, semanticDocument, cancellationToken).ConfigureAwait(false);
 
            // Now, try to infer a possible base type for this new class/interface.
            InferBaseType(service, semanticDocument, cancellationToken);
            IsInterface = GenerateInterface(service);
            IsStruct = GenerateStruct(service, semanticModel, cancellationToken);
            IsAttribute = BaseTypeOrInterfaceOpt != null && BaseTypeOrInterfaceOpt.Equals(semanticModel.Compilation.AttributeType());
            IsException = BaseTypeOrInterfaceOpt != null && BaseTypeOrInterfaceOpt.Equals(semanticModel.Compilation.ExceptionType());
            IsMembersWithModule = generateTypeServiceStateOptions.IsMembersWithModule;
            IsTypeGeneratedIntoNamespaceFromMemberAccess = generateTypeServiceStateOptions.IsTypeGeneratedIntoNamespaceFromMemberAccess;
            IsInterfaceOrEnumNotAllowedInTypeContext = generateTypeServiceStateOptions.IsInterfaceOrEnumNotAllowedInTypeContext;
            IsDelegateAllowed = generateTypeServiceStateOptions.IsDelegateAllowed;
            IsDelegateOnly = generateTypeServiceStateOptions.IsDelegateOnly;
            IsEnumNotAllowed = generateTypeServiceStateOptions.IsEnumNotAllowed;
            DelegateMethodSymbol = generateTypeServiceStateOptions.DelegateCreationMethodSymbol;
            IsClassInterfaceTypes = generateTypeServiceStateOptions.IsClassInterfaceTypes;
            IsSimpleNameGeneric = service.IsGenericName(SimpleName);
            PropertiesToGenerate = generateTypeServiceStateOptions.PropertiesToGenerate;
 
            if (IsAttribute && TypeToGenerateInOpt.GetAllTypeParameters().Any())
            {
                TypeToGenerateInOpt = null;
            }
 
            return TypeToGenerateInOpt != null || NamespaceToGenerateInOpt != null;
        }
 
        private void InferBaseType(
            TService service,
            SemanticDocument document,
            CancellationToken cancellationToken)
        {
            // See if we can find a possible base type for the type being generated.
            // NOTE(cyrusn): I currently limit this to when we have an object creation node.
            // That's because that's when we would have an expression that could be converted to
            // something else.  i.e. if the user writes "IList<int> list = new Goo()" then we can
            // infer a base interface for 'Goo'.  However, if they write "IList<int> list = Goo"
            // then we don't really want to infer a base type for 'Goo'.
 
            // However, there are a few other cases were we can infer a base type.
            var syntaxFacts = document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
            if (service.IsInCatchDeclaration(NameOrMemberAccessExpression))
            {
                SetBaseType(this.Compilation.ExceptionType());
            }
            else if (syntaxFacts.IsNameOfAttribute(NameOrMemberAccessExpression))
            {
                SetBaseType(this.Compilation.AttributeType());
            }
            else
            {
                var expr = ObjectCreationExpressionOpt ?? NameOrMemberAccessExpression;
                if (expr != null)
                {
                    var typeInference = document.Document.GetRequiredLanguageService<ITypeInferenceService>();
                    var baseTypes = typeInference.InferTypes(document.SemanticModel, expr, cancellationToken);
                    foreach (var baseType in baseTypes)
                    {
                        if (this.BaseTypeOrInterfaceOpt is not null)
                            break;
 
                        SetBaseType(baseType as INamedTypeSymbol);
                    }
                }
            }
        }
 
        private void SetBaseType(INamedTypeSymbol? baseType)
        {
            if (baseType == null)
                return;
 
            // A base type need to be non class or interface type.
            if (baseType.IsSealed || baseType.IsStatic)
                return;
 
            if (baseType.TypeKind is not TypeKind.Class and not TypeKind.Interface)
                return;
 
            // 'Object' is redundant to derive from (and we're prefer a more specialized typed if we can infer one).
            // We also filter out non-sealed types that a normal user type still isn't allow to explicit derive from.
            if (baseType.SpecialType is
                    SpecialType.System_Object or
                    SpecialType.System_Array or
                    SpecialType.System_Delegate or
                    SpecialType.System_MulticastDelegate or
                    SpecialType.System_ValueType or
                    SpecialType.System_Enum)
            {
                return;
            }
 
            // Strip off top-level nullability since we can't put top-level nullability into the base list. We will still include nested nullability
            // if you're deriving some interface like IEnumerable<string?>.
            BaseTypeOrInterfaceOpt = (INamedTypeSymbol)baseType.WithNullableAnnotation(NullableAnnotation.None);
        }
 
        private bool GenerateStruct(TService service, SemanticModel semanticModel, CancellationToken cancellationToken)
            => service.IsInValueTypeConstraintContext(semanticModel, NameOrMemberAccessExpression, cancellationToken);
 
        private bool GenerateInterface(TService service)
        {
            if (!IsAttribute &&
                !IsException &&
                Name.LooksLikeInterfaceName() &&
                ObjectCreationExpressionOpt == null &&
                (BaseTypeOrInterfaceOpt == null || BaseTypeOrInterfaceOpt.TypeKind == TypeKind.Interface))
            {
                return true;
            }
 
            return service.IsInInterfaceList(NameOrMemberAccessExpression);
        }
 
        private async Task DetermineNamespaceOrTypeToGenerateInAsync(
            TService service,
            SemanticDocument document,
            CancellationToken cancellationToken)
        {
            DetermineNamespaceOrTypeToGenerateInWorker(service, document.SemanticModel, cancellationToken);
 
            // Can only generate into a type if it's a class and it's from source.
            if (TypeToGenerateInOpt != null)
            {
                if (TypeToGenerateInOpt.TypeKind is not TypeKind.Class and
                    not TypeKind.Module)
                {
                    TypeToGenerateInOpt = null;
                }
                else
                {
                    var symbol = await SymbolFinder.FindSourceDefinitionAsync(TypeToGenerateInOpt, document.Project.Solution, cancellationToken).ConfigureAwait(false);
                    if (symbol == null ||
                        !symbol.IsKind(SymbolKind.NamedType) ||
                        !symbol.Locations.Any(static loc => loc.IsInSource))
                    {
                        TypeToGenerateInOpt = null;
                        return;
                    }
 
                    var sourceTreeToBeGeneratedIn = symbol.Locations.First(loc => loc.IsInSource).SourceTree;
                    var documentToBeGeneratedIn = document.Project.Solution.GetDocument(sourceTreeToBeGeneratedIn);
 
                    if (documentToBeGeneratedIn == null)
                    {
                        TypeToGenerateInOpt = null;
                        return;
                    }
 
                    // If the 2 documents are in different project then we must have Public Accessibility.
                    // If we are generating in a website project, we also want to type to be public so the 
                    // designer files can access the type.
                    if (documentToBeGeneratedIn.Project != document.Project)
                    {
                        IsPublicAccessibilityForTypeGeneration = true;
                    }
 
                    TypeToGenerateInOpt = (INamedTypeSymbol)symbol;
                }
            }
 
            if (TypeToGenerateInOpt != null)
            {
                if (!CodeGenerator.CanAdd(document.Project.Solution, TypeToGenerateInOpt, cancellationToken))
                {
                    TypeToGenerateInOpt = null;
                }
            }
        }
 
        private bool DetermineNamespaceOrTypeToGenerateInWorker(
            TService service,
            SemanticModel semanticModel,
            CancellationToken cancellationToken)
        {
            // If we're on the right of a dot, see if we can figure out what's on the left.  If
            // it doesn't bind to a type or a namespace, then we can't proceed.
            if (SimpleName != NameOrMemberAccessExpression)
            {
                return DetermineNamespaceOrTypeToGenerateIn(
                    service, semanticModel,
                    service.GetLeftSideOfDot(SimpleName), cancellationToken);
            }
            else
            {
                // The name is standing alone.  We can either generate the type into our
                // containing type, or into our containing namespace.
                //
                // TODO(cyrusn): We need to make this logic work if the type is in the
                // base/interface list of a type.
                var format = SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted);
                TypeToGenerateInOpt = service.DetermineTypeToGenerateIn(semanticModel, SimpleName, cancellationToken);
                if (TypeToGenerateInOpt != null)
                {
                    NamespaceToGenerateInOpt = TypeToGenerateInOpt.ContainingNamespace.ToDisplayString(format);
                }
                else
                {
                    var namespaceSymbol = semanticModel.GetEnclosingNamespace(SimpleName.SpanStart, cancellationToken);
                    if (namespaceSymbol != null)
                    {
                        NamespaceToGenerateInOpt = namespaceSymbol.ToDisplayString(format);
                    }
                }
            }
 
            return true;
        }
 
        private bool DetermineNamespaceOrTypeToGenerateIn(
            TService service,
            SemanticModel semanticModel,
            TExpressionSyntax leftSide,
            CancellationToken cancellationToken)
        {
            var leftSideInfo = semanticModel.GetSymbolInfo(leftSide, cancellationToken);
 
            if (leftSideInfo.Symbol != null)
            {
                var symbol = leftSideInfo.Symbol;
 
                if (symbol is INamespaceSymbol)
                {
                    NamespaceToGenerateInOpt = symbol.ToNameDisplayString();
                    return true;
                }
                else if (symbol is INamedTypeSymbol)
                {
                    // TODO: Code coverage
                    TypeToGenerateInOpt = (INamedTypeSymbol)symbol.OriginalDefinition;
                    return true;
                }
 
                // We bound to something other than a namespace or named type.  Can't generate a
                // type inside this.
                return false;
            }
            else
            {
                // If it's a dotted name, then perhaps it's a namespace.  i.e. the user wrote
                // "new Goo.Bar.Baz()".  In this case we want to generate a namespace for
                // "Goo.Bar".
                if (service.TryGetNameParts(leftSide, out var nameParts))
                {
                    NamespaceToGenerateInOpt = string.Join(".", nameParts);
                    return true;
                }
            }
 
            return false;
        }
    }
 
    protected class GenerateTypeServiceStateOptions
    {
        public TExpressionSyntax? NameOrMemberAccessExpression { get; set; }
        public TObjectCreationExpressionSyntax? ObjectCreationExpressionOpt { get; set; }
        public IMethodSymbol? DelegateCreationMethodSymbol { get; set; }
        public List<TSimpleNameSyntax> PropertiesToGenerate { get; }
        public bool IsMembersWithModule { get; set; }
        public bool IsTypeGeneratedIntoNamespaceFromMemberAccess { get; set; }
        public bool IsInterfaceOrEnumNotAllowedInTypeContext { get; set; }
        public bool IsDelegateAllowed { get; set; }
        public bool IsEnumNotAllowed { get; set; }
        public bool IsDelegateOnly { get; internal set; }
        public bool IsClassInterfaceTypes { get; internal set; }
 
        public GenerateTypeServiceStateOptions()
        {
            IsMembersWithModule = false;
            PropertiesToGenerate = [];
            IsTypeGeneratedIntoNamespaceFromMemberAccess = false;
            IsInterfaceOrEnumNotAllowedInTypeContext = false;
            IsDelegateAllowed = true;
            IsEnumNotAllowed = false;
            IsDelegateOnly = false;
        }
    }
}