File: GenerateMember\GenerateVariable\CSharpGenerateVariableService.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.
 
#nullable disable
 
using System;
using System.Composition;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.GenerateMember.GenerateVariable;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.GenerateMember.GenerateVariable;
 
using static SyntaxFactory;
 
[ExportLanguageService(typeof(IGenerateVariableService), LanguageNames.CSharp), Shared]
internal sealed partial class CSharpGenerateVariableService :
    AbstractGenerateVariableService<CSharpGenerateVariableService, SimpleNameSyntax, ExpressionSyntax>
{
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public CSharpGenerateVariableService()
    {
    }
 
    protected override bool IsExplicitInterfaceGeneration(SyntaxNode node)
        => node is PropertyDeclarationSyntax;
 
    protected override bool IsIdentifierNameGeneration(SyntaxNode node)
        => node is IdentifierNameSyntax identifierName && !IsProbablySyntacticConstruct(identifierName.Identifier);
 
    private static bool IsProbablySyntacticConstruct(SyntaxToken token)
    {
        // Technically all C# contextual keywords are valid member names.
        // However some of them start various syntactic constructs
        // and we don't want to show "Generate <member name>" codefix for them:
        // 1. "from" starts LINQ expression
        // 2. "nameof" is probably nameof(some_name)
        // 3. "async" can start a delegate declaration
        // 4. "await" starts await expression
        // 5. "var" is used in constructions like "var x = ..."
        // The list can be expanded in the future if necessary
        // This method tells if the given SyntaxToken is one of the cases above
        var contextualKind = SyntaxFacts.GetContextualKeywordKind(token.ValueText);
 
        return contextualKind is SyntaxKind.FromKeyword or
                              SyntaxKind.NameOfKeyword or
                              SyntaxKind.AsyncKeyword or
                              SyntaxKind.AwaitKeyword or
                              SyntaxKind.VarKeyword;
    }
 
    protected override bool ContainingTypesOrSelfHasUnsafeKeyword(INamedTypeSymbol containingType)
        => containingType.ContainingTypesOrSelfHasUnsafeKeyword();
 
    protected override bool TryInitializeExplicitInterfaceState(
        SemanticDocument document, SyntaxNode node, CancellationToken cancellationToken,
        out SyntaxToken identifierToken, out IPropertySymbol propertySymbol, out INamedTypeSymbol typeToGenerateIn)
    {
        var propertyDeclaration = (PropertyDeclarationSyntax)node;
        identifierToken = propertyDeclaration.Identifier;
 
        if (propertyDeclaration.ExplicitInterfaceSpecifier != null)
        {
            var semanticModel = document.SemanticModel;
            propertySymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, cancellationToken);
            if (propertySymbol != null && !propertySymbol.ExplicitInterfaceImplementations.Any())
            {
                var info = semanticModel.GetTypeInfo(propertyDeclaration.ExplicitInterfaceSpecifier.Name, cancellationToken);
                typeToGenerateIn = info.Type as INamedTypeSymbol;
                return typeToGenerateIn != null;
            }
        }
 
        identifierToken = default;
        propertySymbol = null;
        typeToGenerateIn = null;
        return false;
    }
 
    protected override bool TryInitializeIdentifierNameState(
        SemanticDocument document, SimpleNameSyntax identifierName, CancellationToken cancellationToken,
        out SyntaxToken identifierToken, out ExpressionSyntax simpleNameOrMemberAccessExpression, out bool isInExecutableBlock, out bool isConditionalAccessExpression)
    {
        identifierToken = identifierName.Identifier;
        if (identifierToken.ValueText != string.Empty &&
            !IsProbablyGeneric(identifierName, cancellationToken))
        {
            var memberAccess = identifierName.Parent as MemberAccessExpressionSyntax;
            var conditionalMemberAccess = identifierName.Parent.Parent as ConditionalAccessExpressionSyntax;
            if (memberAccess?.Name == identifierName)
            {
                simpleNameOrMemberAccessExpression = memberAccess;
            }
            else if ((conditionalMemberAccess?.WhenNotNull as MemberBindingExpressionSyntax)?.Name == identifierName)
            {
                simpleNameOrMemberAccessExpression = conditionalMemberAccess;
            }
            else
            {
                simpleNameOrMemberAccessExpression = identifierName;
            }
 
            // If we're being invoked, then don't offer this, offer generate method instead.
            // Note: we could offer to generate a field with a delegate type.  However, that's
            // very esoteric and probably not what most users want.
            if (!IsLegal(document, simpleNameOrMemberAccessExpression, cancellationToken))
            {
                isInExecutableBlock = false;
                isConditionalAccessExpression = false;
                return false;
            }
 
            var block = identifierName.GetAncestor<BlockSyntax>();
            isInExecutableBlock = block != null && !block.OverlapsHiddenPosition(cancellationToken);
            isConditionalAccessExpression = conditionalMemberAccess != null;
            return true;
        }
 
        identifierToken = default;
        simpleNameOrMemberAccessExpression = null;
        isInExecutableBlock = false;
        isConditionalAccessExpression = false;
        return false;
    }
 
    private static bool IsProbablyGeneric(SimpleNameSyntax identifierName, CancellationToken cancellationToken)
    {
        if (identifierName.IsKind(SyntaxKind.GenericName))
        {
            return true;
        }
 
        // We might have something of the form:   Goo < Bar.
        // In this case, we would want to generate offer a member called 'Goo'.  however, if we have
        // something like "Goo < string >" then that's clearly something generic and we don't want
        // to offer to generate a member there.
        var localRoot = identifierName.GetAncestor<StatementSyntax>() ??
                        identifierName.GetAncestor<MemberDeclarationSyntax>() ??
                        identifierName.SyntaxTree.GetRoot(cancellationToken);
 
        // In order to figure this out (without writing our own parser), we just try to parse out a
        // type name here.  If we get a generic name back, without any errors, then we'll assume the
        // user really is typing a generic name, and thus should not get recommendations to create a
        // variable.
        var localText = localRoot.ToString();
        var startIndex = identifierName.Span.Start - localRoot.Span.Start;
 
        var parsedType = ParseTypeName(localText, startIndex, consumeFullText: false);
 
        return parsedType.IsKind(SyntaxKind.GenericName) && !parsedType.ContainsDiagnostics;
    }
 
    private static bool IsLegal(
        SemanticDocument document,
        ExpressionSyntax expression,
        CancellationToken cancellationToken)
    {
        // TODO(cyrusn): Consider supporting this at some point.  It is difficult because we'd
        // need to replace the identifier typed with the fully qualified name of the field we
        // were generating.
        if (expression.IsParentKind(SyntaxKind.AttributeArgument))
        {
            return false;
        }
 
        if (expression.IsParentKind(SyntaxKind.ConditionalAccessExpression))
        {
            return true;
        }
 
        if (expression.IsParentKind(SyntaxKind.IsPatternExpression))
        {
            return true;
        }
 
        if (expression.Parent is (kind: SyntaxKind.NameColon or SyntaxKind.ExpressionColon) &&
            expression.Parent.IsParentKind(SyntaxKind.Subpattern))
        {
            return true;
        }
 
        if (expression.IsParentKind(SyntaxKind.ConstantPattern))
        {
            return true;
        }
 
        return expression.CanReplaceWithLValue(document.SemanticModel, cancellationToken);
    }
 
    protected override bool TryConvertToLocalDeclaration(ITypeSymbol type, SyntaxToken identifierToken, SemanticModel semanticModel, CancellationToken cancellationToken, out SyntaxNode newRoot)
    {
        var token = identifierToken;
        var node = identifierToken.Parent as IdentifierNameSyntax;
        if (node.IsLeftSideOfAssignExpression() && node.Parent.IsParentKind(SyntaxKind.ExpressionStatement))
        {
            var assignExpression = (AssignmentExpressionSyntax)node.Parent;
            var expressionStatement = (StatementSyntax)assignExpression.Parent;
 
            var declarationStatement = LocalDeclarationStatement(
                VariableDeclaration(
                    type.GenerateTypeSyntax(),
                    [VariableDeclarator(token, null, EqualsValueClause(
                        assignExpression.OperatorToken, assignExpression.Right))]));
            declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation);
 
            var root = token.GetAncestor<CompilationUnitSyntax>();
            newRoot = root.ReplaceNode(expressionStatement, declarationStatement);
 
            return true;
        }
 
        newRoot = null;
        return false;
    }
}