File: Completion\KeywordRecommenders\RefKeywordRecommender.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.
 
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders;
 
internal class RefKeywordRecommender : AbstractSyntacticSingleKeywordRecommender
{
    public RefKeywordRecommender()
        : base(SyntaxKind.RefKeyword)
    {
    }
 
    /// <summary>
    /// Same as <see cref="SyntaxKindSet.AllMemberModifiers"/> with ref specific exclusions
    /// </summary>
    private static readonly ISet<SyntaxKind> RefMemberModifiers = new HashSet<SyntaxKind>(SyntaxFacts.EqualityComparer)
        {
            SyntaxKind.AbstractKeyword,
            // SyntaxKind.AsyncKeyword,    // async methods cannot be byref
            SyntaxKind.ExternKeyword,
            SyntaxKind.InternalKeyword,
            SyntaxKind.NewKeyword,
            SyntaxKind.OverrideKeyword,
            SyntaxKind.PublicKeyword,
            SyntaxKind.PrivateKeyword,
            SyntaxKind.ProtectedKeyword,
            // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref
            SyntaxKind.SealedKeyword,
            SyntaxKind.StaticKeyword,
            SyntaxKind.UnsafeKeyword,
            SyntaxKind.VirtualKeyword,
            // SyntaxKind.VolatileKeyword, // fields cannot be byref
        };
 
    /// <summary>
    /// Same as <see cref="SyntaxKindSet.AllGlobalMemberModifiers"/> with ref-specific exclusions
    /// </summary>
    private static readonly ISet<SyntaxKind> RefGlobalMemberModifiers = new HashSet<SyntaxKind>(SyntaxFacts.EqualityComparer)
        {
            // SyntaxKind.AsyncKeyword,    // async local functions cannot be byref
            SyntaxKind.ExternKeyword,
            SyntaxKind.InternalKeyword,
            SyntaxKind.NewKeyword,
            SyntaxKind.OverrideKeyword,
            SyntaxKind.PublicKeyword,
            SyntaxKind.PrivateKeyword,
            SyntaxKind.ReadOnlyKeyword,
            SyntaxKind.StaticKeyword,
            SyntaxKind.UnsafeKeyword,
            SyntaxKind.VolatileKeyword,
        };
 
    /// <summary>
    /// Same as <see cref="SyntaxKindSet.AllGlobalMemberModifiers"/> with ref-specific exclusions for C# script
    /// </summary>
    private static readonly ISet<SyntaxKind> RefGlobalMemberScriptModifiers = new HashSet<SyntaxKind>(SyntaxFacts.EqualityComparer)
        {
            // SyntaxKind.AsyncKeyword,    // async methods cannot be byref
            SyntaxKind.ExternKeyword,
            SyntaxKind.InternalKeyword,
            SyntaxKind.NewKeyword,
            SyntaxKind.OverrideKeyword,
            SyntaxKind.PublicKeyword,
            SyntaxKind.PrivateKeyword,
            // SyntaxKind.ReadOnlyKeyword, // fields cannot be byref
            SyntaxKind.StaticKeyword,
            SyntaxKind.UnsafeKeyword,
            // SyntaxKind.VolatileKeyword, // fields cannot be byref
        };
 
    protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken)
    {
        var syntaxTree = context.SyntaxTree;
        return
            IsRefParameterModifierContext(position, context) ||
            IsValidContextForType(context, cancellationToken) ||
            syntaxTree.IsAnonymousMethodParameterModifierContext(position, context.LeftToken) ||
            syntaxTree.IsPossibleLambdaParameterModifierContext(position, context.LeftToken, cancellationToken) ||
            context.TargetToken.IsConstructorOrMethodParameterArgumentContext() ||
            context.TargetToken.IsXmlCrefParameterModifierContext() ||
            IsValidNewByRefContext(syntaxTree, position, context, cancellationToken) ||
            IsAllowsRefStructConstraintContext(context);
    }
 
    private static bool IsAllowsRefStructConstraintContext(CSharpSyntaxContext context)
    {
        // cases:
        //    where T : allows |
        //    where T : struct, allows |
        //    where T : new(), allows |
        //    where T : Goo, allows |
        // note: 'allows ref struct' can't come after a 'class' constraint.
 
        var token = context.TargetToken;
 
        if (token.Kind() == SyntaxKind.IdentifierToken && SyntaxFacts.GetContextualKeywordKind((string)token.Value!) == SyntaxKind.AllowsKeyword &&
            token.Parent is IdentifierNameSyntax identifier && identifier.Identifier == token &&
            identifier.Parent is TypeConstraintSyntax typeConstraint && typeConstraint.Type == identifier &&
            typeConstraint.Parent is TypeParameterConstraintClauseSyntax constraintClause)
        {
            if (!constraintClause.Constraints
                    .OfType<ClassOrStructConstraintSyntax>()
                    .Any(c => c.ClassOrStructKeyword.Kind() == SyntaxKind.ClassKeyword))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool IsRefParameterModifierContext(int position, CSharpSyntaxContext context)
    {
        if (context.SyntaxTree.IsParameterModifierContext(
                position, context.LeftToken, includeOperators: false, out var parameterIndex, out var previousModifier))
        {
            if (previousModifier is SyntaxKind.None or SyntaxKind.ScopedKeyword)
            {
                return true;
            }
 
            if (previousModifier == SyntaxKind.ThisKeyword &&
                parameterIndex == 0 &&
                context.SyntaxTree.IsPossibleExtensionMethodContext(context.LeftToken))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static bool IsValidNewByRefContext(SyntaxTree syntaxTree, int position, CSharpSyntaxContext context, CancellationToken cancellationToken)
    {
        return
            IsValidRefExpressionContext(context) ||
            context.IsDelegateReturnTypeContext ||
            syntaxTree.IsGlobalMemberDeclarationContext(position, syntaxTree.IsScript() ? RefGlobalMemberScriptModifiers : RefGlobalMemberModifiers, cancellationToken) ||
            context.IsMemberDeclarationContext(
                validModifiers: RefMemberModifiers,
                validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations,
                canBePartial: true,
                cancellationToken: cancellationToken);
    }
 
    private static bool IsValidRefExpressionContext(CSharpSyntaxContext context)
    {
        // {
        //     ref var x ...
        // 
        if (context.IsStatementContext)
        {
            return true;
        }
 
        // 
        //  ref Goo(int x, ...
        // 
        if (context.IsGlobalStatementContext)
        {
            return true;
        }
 
        var token = context.TargetToken;
 
        switch (token.Kind())
        {
            // {
            //     return ref  ...
            // 
            case SyntaxKind.ReturnKeyword:
                return true;
 
            // scoped ref ...
            case SyntaxKind.ScopedKeyword:
            case SyntaxKind.IdentifierToken when token.Text == "scoped":
                return true;
 
            // {
            //     () => ref ...
            // 
            case SyntaxKind.EqualsGreaterThanToken:
                return true;
 
            // {
            //     for (ref var x ...
            //
            //     foreach (ref var x ...
            //
            case SyntaxKind.OpenParenToken:
                var previous = token.GetPreviousToken(includeSkipped: true);
                return previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword;
 
            // {
            //     ref var x = ref
            // 
            case SyntaxKind.EqualsToken:
                var parent = token.Parent;
                return parent?.Kind() == SyntaxKind.SimpleAssignmentExpression
                    || parent?.Parent?.Kind() == SyntaxKind.VariableDeclarator;
 
            // {
            //     var x = true ?
            //     var x = true ? ref y :
            case SyntaxKind.QuestionToken:
            case SyntaxKind.ColonToken:
                return token.Parent?.Kind() == SyntaxKind.ConditionalExpression;
        }
 
        return false;
    }
 
    private static bool IsValidContextForType(CSharpSyntaxContext context, CancellationToken cancellationToken)
    {
        return context.IsTypeDeclarationContext(validModifiers: SyntaxKindSet.AllTypeModifiers,
            validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations, canBePartial: true, cancellationToken);
    }
}