File: IntroduceVariable\AbstractIntroduceVariableService.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.
 
#nullable disable
 
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.IntroduceVariable;
 
internal abstract partial class AbstractIntroduceVariableService<TService, TExpressionSyntax, TTypeSyntax, TTypeDeclarationSyntax, TQueryExpressionSyntax, TNameSyntax>
{
    private sealed partial class State(TService service, SemanticDocument document, CodeCleanupOptions options)
    {
        public SemanticDocument Document { get; } = document;
        public CodeCleanupOptions Options { get; } = options;
        public TExpressionSyntax Expression { get; private set; }
 
        public bool InAttributeContext { get; private set; }
        public bool InBlockContext { get; private set; }
        public bool InConstructorInitializerContext { get; private set; }
        public bool InGlobalStatementContext { get; private set; }
        public bool InFieldContext { get; private set; }
        public bool InParameterContext { get; private set; }
        public bool InQueryContext { get; private set; }
        public bool InExpressionBodiedMemberContext { get; private set; }
        public bool InAutoPropertyInitializerContext { get; private set; }
 
        public bool IsConstant { get; private set; }
 
        private SemanticMap _semanticMap;
        private readonly TService _service = service;
 
        public static async Task<State> GenerateAsync(
            TService service,
            SemanticDocument document,
            CodeCleanupOptions options,
            TextSpan textSpan,
            CancellationToken cancellationToken)
        {
            var state = new State(service, document, options);
            if (!await state.TryInitializeAsync(document, textSpan, cancellationToken).ConfigureAwait(false))
            {
                return null;
            }
 
            return state;
        }
 
        private async Task<bool> TryInitializeAsync(
            SemanticDocument document,
            TextSpan textSpan,
            CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            Expression = await document.Document.TryGetRelevantNodeAsync<TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false);
            if (Expression == null || CodeRefactoringHelpers.IsNodeUnderselected(Expression, textSpan))
                return false;
 
            // Don't introduce constant for another constant. Doesn't apply to sub-expression of constant.
            if (IsInitializerOfConstant(document, Expression))
                return false;
 
            // Too noisy to offer introduce-local on `this/me`.
            var syntaxFacts = document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
            if (syntaxFacts.IsThisExpression(Expression))
                return false;
 
            var expressionType = Document.SemanticModel.GetTypeInfo(Expression, cancellationToken).Type;
            if (expressionType is IErrorTypeSymbol)
                return false;
 
            // Inside an attribute we can only extract out constant values (so no arrays or 'System.Type's).
            if (this.IsInAttributeContext() &&
                !Document.SemanticModel.GetConstantValue(Expression, cancellationToken).HasValue)
            {
                return false;
            }
 
            var containingType = Expression.AncestorsAndSelf()
                .Select(n => Document.SemanticModel.GetDeclaredSymbol(n, cancellationToken))
                .OfType<INamedTypeSymbol>()
                .FirstOrDefault();
 
            containingType ??= Document.SemanticModel.Compilation.ScriptClass;
 
            if (containingType?.TypeKind is TypeKind.Interface)
                return false;
 
            if (containingType is null)
            {
                var globalStatement = Expression.AncestorsAndSelf().FirstOrDefault(syntaxFacts.IsGlobalStatement);
                if (globalStatement != null)
                {
                    InGlobalStatementContext = true;
                    return true;
                }
 
                return false;
            }
 
            if (!CanIntroduceVariable(textSpan.IsEmpty, cancellationToken))
                return false;
 
            IsConstant = IsExpressionConstant(Document, Expression, _service, cancellationToken);
 
            // Note: the ordering of these clauses are important.  They go, generally, from
            // innermost to outermost order.
            if (IsInQueryContext(cancellationToken))
            {
                if (CanGenerateInto<TQueryExpressionSyntax>(cancellationToken))
                {
                    InQueryContext = true;
                    return true;
                }
 
                return false;
            }
 
            if (IsInConstructorInitializerContext(cancellationToken))
            {
                if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
                {
                    InConstructorInitializerContext = true;
                    return true;
                }
 
                return false;
            }
 
            var enclosingBlocks = _service.GetContainingExecutableBlocks(Expression);
            if (enclosingBlocks.Any())
            {
                // If we're inside a block, then don't even try the other options (like field,
                // constructor initializer, etc.).  This is desirable behavior.  If we're in a 
                // block in a field, then we're in a lambda, and we want to offer to generate
                // a local, and not a field.
                if (IsInBlockContext(cancellationToken))
                {
                    InBlockContext = true;
                    return true;
                }
 
                return false;
            }
 
            // NOTE: All checks from this point forward are intentionally ordered to be AFTER the check for Block Context.
 
            // If we are inside a block within an Expression bodied member we should generate inside the block, 
            // instead of rewriting a concise expression bodied member to its equivalent that has a body with a block.
            if (_service.IsInExpressionBodiedMember(Expression))
            {
                if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
                {
                    InExpressionBodiedMemberContext = true;
                    return true;
                }
 
                return false;
            }
 
            if (_service.IsInAutoPropertyInitializer(Expression))
            {
                if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
                {
                    InAutoPropertyInitializerContext = true;
                    return true;
                }
 
                return false;
            }
 
            if (CanGenerateInto<TTypeDeclarationSyntax>(cancellationToken))
            {
                if (IsInParameterContext(cancellationToken))
                {
                    InParameterContext = true;
                    return true;
                }
                else if (IsInFieldContext(cancellationToken))
                {
                    InFieldContext = true;
                    return true;
                }
                else if (IsInAttributeContext())
                {
                    InAttributeContext = true;
                    return true;
                }
            }
 
            return false;
 
            static bool IsInitializerOfConstant(SemanticDocument document, TExpressionSyntax expression)
            {
                var syntaxFacts = document.Document.GetRequiredLanguageService<ISyntaxFactsService>();
 
                var current = expression;
                while (syntaxFacts.IsParenthesizedExpression(current.Parent))
                    current = (TExpressionSyntax)current.Parent;
 
                if (!syntaxFacts.IsEqualsValueClause(current.Parent))
                    return false;
 
                var equalsValue = current.Parent;
                if (!syntaxFacts.IsVariableDeclarator(equalsValue.Parent))
                    return false;
 
                var declaration = equalsValue.AncestorsAndSelf().FirstOrDefault(n => syntaxFacts.IsLocalDeclarationStatement(n) || syntaxFacts.IsFieldDeclaration(n));
                if (declaration == null)
                    return false;
 
                var generator = SyntaxGenerator.GetGenerator(document.Document);
                return generator.GetModifiers(declaration).IsConst;
            }
 
            static bool IsExpressionConstant(SemanticDocument document, TExpressionSyntax expression, TService service, CancellationToken cancellationToken)
            {
                if (document.SemanticModel.GetConstantValue(expression, cancellationToken) is { HasValue: true, Value: var value })
                {
                    var syntaxKindsService = document.Document.GetRequiredLanguageService<ISyntaxKindsService>();
                    if (syntaxKindsService.InterpolatedStringExpression == expression.RawKind && value is string)
                    {
                        // Interpolated strings can have constant values, but if it's being converted to a FormattableString
                        // or IFormattable then we cannot treat it as one
                        var typeInfo = document.SemanticModel.GetTypeInfo(expression, cancellationToken);
                        return typeInfo.ConvertedType?.IsFormattableStringOrIFormattable() != true;
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    return false;
                }
            }
        }
 
        public SemanticMap GetSemanticMap(CancellationToken cancellationToken)
        {
            _semanticMap ??= Document.SemanticModel.GetSemanticMap(Expression, cancellationToken);
            return _semanticMap;
        }
 
        private bool CanIntroduceVariable(
            bool isSpanEmpty,
            CancellationToken cancellationToken)
        {
            if (!_service.CanIntroduceVariableFor(Expression))
            {
                return false;
            }
 
            if (isSpanEmpty && Expression is TNameSyntax)
            {
                // to extract a name, you must have a selection (this avoids making the refactoring too noisy)
                return false;
            }
 
            if (Expression is TTypeSyntax and not TNameSyntax)
            {
                // name syntax can introduce variables, but not other type syntaxes
                return false;
            }
 
            // Even though we're creating a variable, we still ask if we can be replaced with an
            // RValue and not an LValue.  This is because introduction of a local adds a *new* LValue
            // location, and we want to ensure that any writes will still happen to the *original*
            // LValue location.  i.e. if you have: "a[1] = b" then you don't want to change that to
            // "var c = a[1]; c = b", as that write is no longer happening into the right LValue.
            //
            // In essence, this says "i can be replaced with an expression as long as I'm not being
            // written to".
            var semanticFacts = Document.Project.Services.GetService<ISemanticFactsService>();
            return semanticFacts.CanReplaceWithRValue(Document.SemanticModel, Expression, cancellationToken);
        }
 
        private bool CanGenerateInto<TSyntax>(CancellationToken cancellationToken)
            where TSyntax : SyntaxNode
        {
            if (Document.SemanticModel.Compilation.ScriptClass != null)
            {
                return true;
            }
 
            var syntax = Expression.GetAncestor<TSyntax>();
            return syntax != null && !syntax.OverlapsHiddenPosition(cancellationToken);
        }
 
        private bool IsInTypeDeclarationOrValidCompilationUnit()
        {
            if (Expression.GetAncestorOrThis<TTypeDeclarationSyntax>() != null)
            {
                return true;
            }
 
            // If we're interactive/script, we can generate into the compilation unit.
            if (Document.Document.SourceCodeKind != SourceCodeKind.Regular)
            {
                return true;
            }
 
            return false;
        }
    }
}