File: Completion\CompletionProviders\DeclarationName\DeclarationNameInfo.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;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.SymbolSpecification;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers.DeclarationName;
 
internal readonly struct NameDeclarationInfo(
    ImmutableArray<SymbolKindOrTypeKind> possibleSymbolKinds,
    Accessibility? accessibility,
    DeclarationModifiers declarationModifiers = default,
    ITypeSymbol? type = null,
    IAliasSymbol? alias = null,
    ISymbol? symbol = null)
{
    private static readonly ImmutableArray<SymbolKindOrTypeKind> s_parameterSyntaxKind =
        [new SymbolKindOrTypeKind(SymbolKind.Parameter)];
 
    private static readonly ImmutableArray<SymbolKindOrTypeKind> s_propertySyntaxKind =
        [new SymbolKindOrTypeKind(SymbolKind.Property)];
 
    private readonly ImmutableArray<SymbolKindOrTypeKind> _possibleSymbolKinds = possibleSymbolKinds;
 
    public readonly DeclarationModifiers Modifiers = declarationModifiers;
    public readonly Accessibility? DeclaredAccessibility = accessibility;
 
    public readonly ITypeSymbol? Type = type;
    public readonly IAliasSymbol? Alias = alias;
    public readonly ISymbol? Symbol = symbol;
 
    public ImmutableArray<SymbolKindOrTypeKind> PossibleSymbolKinds => _possibleSymbolKinds.NullToEmpty();
 
    public static async Task<NameDeclarationInfo> GetDeclarationInfoAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var info = await GetDeclarationInfoWorkerAsync(document, position, cancellationToken).ConfigureAwait(false);
 
        // if we bound to some error type, and that error type itself didn't start with an uppercase letter, then
        // it's almost certainly just an error case where the user was referencing something that was not a type.
        // for example:
        //
        //  goo $$
        //  goo = ...
        //
        // This syntactically looks like a type, but really isn't.  We don't want to offer anything here as it's far
        // more likely to be an error rather than a true new declaration.
        if (info.Type is IErrorTypeSymbol { Name.Length: > 0 } &&
            !char.IsUpper(info.Type.Name[0]))
        {
            return default;
        }
 
        return info;
    }
 
    private static async Task<NameDeclarationInfo> GetDeclarationInfoWorkerAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken).GetPreviousTokenIfTouchingWord(position);
        var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.SpanStart, cancellationToken).ConfigureAwait(false);
        var typeInferenceService = document.GetRequiredLanguageService<ITypeInferenceService>();
 
        if (IsTupleTypeElement(token, semanticModel, cancellationToken, out var result)
            || IsPrimaryConstructorParameter(token, semanticModel, cancellationToken, out result)
            || IsParameterDeclaration(token, semanticModel, cancellationToken, out result)
            || IsTypeParameterDeclaration(token, out result)
            || IsLocalFunctionDeclaration(token, semanticModel, cancellationToken, out result)
            || IsLocalVariableDeclaration(token, semanticModel, cancellationToken, out result)
            || IsEmbeddedVariableDeclaration(token, semanticModel, cancellationToken, out result)
            || IsForEachVariableDeclaration(token, semanticModel, cancellationToken, out result)
            || IsIncompleteMemberDeclaration(token, semanticModel, cancellationToken, out result)
            || IsFieldDeclaration(token, semanticModel, cancellationToken, out result)
            || IsMethodDeclaration(token, semanticModel, cancellationToken, out result)
            || IsPropertyDeclaration(token, semanticModel, cancellationToken, out result)
            || IsPossibleOutVariableDeclaration(token, semanticModel, typeInferenceService, cancellationToken, out result)
            || IsTupleLiteralElement(token, semanticModel, cancellationToken, out result)
            || IsPossibleLocalVariableOrFunctionDeclaration(token, semanticModel, cancellationToken, out result)
            || IsPatternMatching(token, semanticModel, cancellationToken, out result))
        {
            return result;
        }
 
        return default;
    }
 
    private static bool IsTupleTypeElement(
        SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsFollowingTypeOrComma<TupleElementSyntax>(
            token,
            semanticModel,
            tupleElement => tupleElement.Type,
            _ => default(SyntaxTokenList),
            _ => [new SymbolKindOrTypeKind(SymbolKind.Local)], cancellationToken);
 
        return result.Type != null;
    }
 
    private static bool IsTupleLiteralElement(
        SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        // Incomplete code like
        // void Do()
        // {
        //    (System.Array array, System.Action $$ 
        // gets parsed as a tuple expression. We can figure out the type in such cases.
        // For a legit tuple expression we can't provide any completion.
        if (token.GetAncestor(node => node.IsKind(SyntaxKind.TupleExpression)) != null)
        {
            result = IsFollowingTypeOrComma<ArgumentSyntax>(
                token,
                semanticModel,
                GetNodeDenotingTheTypeOfTupleArgument,
                _ => default(SyntaxTokenList),
                _ => [new SymbolKindOrTypeKind(SymbolKind.Local)], cancellationToken);
            return result.Type != null;
        }
 
        result = default;
        return false;
    }
 
    private static bool IsPossibleOutVariableDeclaration(SyntaxToken token, SemanticModel semanticModel,
        ITypeInferenceService typeInferenceService, CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        if (token.IsKind(SyntaxKind.IdentifierToken) &&
            token.Parent.IsKind(SyntaxKind.IdentifierName))
        {
            var argument = token.Parent.Parent as ArgumentSyntax  // var is child of ArgumentSyntax, eg. Goo(out var $$
                ?? token.Parent.Parent?.Parent as ArgumentSyntax; // var is child of DeclarationExpression 
 
            // under ArgumentSyntax, eg. Goo(out var a$$
            if (argument is { RefOrOutKeyword: SyntaxToken(SyntaxKind.OutKeyword) })
            {
                var type = typeInferenceService.InferType(semanticModel, argument.SpanStart, objectAsDefault: false, cancellationToken: cancellationToken);
                if (type != null)
                {
                    var parameter = CSharpSemanticFacts.Instance.FindParameterForArgument(
                        semanticModel, argument, allowUncertainCandidates: true, allowParams: false, cancellationToken);
 
                    result = new NameDeclarationInfo(
                        [new SymbolKindOrTypeKind(SymbolKind.Local)],
                        Accessibility.NotApplicable,
                        type: type,
                        symbol: parameter);
                    return true;
                }
            }
        }
 
        result = default;
        return false;
    }
 
    private static bool IsPossibleLocalVariableOrFunctionDeclaration(
        SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<ExpressionStatementSyntax>(
            token, semanticModel,
            e => e.Expression,
            _ => default,
            _ => [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)],
            cancellationToken,
            out var expression);
 
        if (result.Type is null || expression is null)
            return false;
 
        // we have something like `x.y $$`.
        //
        // For this to actually be the start of a local declaration or function x.y needs to bind to an actual
        // type symbol, not just any arbitrary expression that might have a type (e.g. `Console.BackgroundColor $$).
        var symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol();
        return symbol is ITypeSymbol;
    }
 
    private static bool IsPropertyDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<PropertyDeclarationSyntax>(
            token,
            semanticModel,
            m => m.Type,
            m => m.Modifiers,
            GetPossibleMemberDeclarations,
            cancellationToken);
 
        return result.Type != null;
    }
 
    private static bool IsMethodDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<MethodDeclarationSyntax>(
            token,
            semanticModel,
            m => m.ReturnType,
            m => m.Modifiers,
            GetPossibleMemberDeclarations,
            cancellationToken);
 
        return result.Type != null;
    }
 
    private static NameDeclarationInfo IsFollowingTypeOrComma<TSyntaxNode>(
        SyntaxToken token,
        SemanticModel semanticModel,
        Func<TSyntaxNode, SyntaxNode?> typeSyntaxGetter,
        Func<TSyntaxNode, SyntaxTokenList?> modifierGetter,
        Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>> possibleDeclarationComputer,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        if (!IsPossibleTypeToken(token) && !token.IsKind(SyntaxKind.CommaToken))
        {
            return default;
        }
 
        var target = token.GetAncestor<TSyntaxNode>();
        if (target == null)
        {
            return default;
        }
 
        if (token.IsKind(SyntaxKind.CommaToken) && token.Parent != target)
        {
            return default;
        }
 
        var typeSyntax = typeSyntaxGetter(target);
        if (typeSyntax == null)
        {
            return default;
        }
 
        if (!token.IsKind(SyntaxKind.CommaToken) && token != typeSyntax.GetLastToken())
        {
            return default;
        }
 
        var modifiers = modifierGetter(target);
        if (modifiers == null)
        {
            return default;
        }
 
        return new NameDeclarationInfo(
            possibleDeclarationComputer(GetDeclarationModifiers(modifiers.Value)),
            GetAccessibility(modifiers.Value),
            GetDeclarationModifiers(modifiers.Value),
            semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type,
            semanticModel.GetAliasInfo(typeSyntax, cancellationToken));
    }
 
    private static NameDeclarationInfo IsLastTokenOfType<TSyntaxNode>(
        SyntaxToken token,
        SemanticModel semanticModel,
        Func<TSyntaxNode, SyntaxNode?> typeSyntaxGetter,
        Func<TSyntaxNode, SyntaxTokenList> modifierGetter,
        Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>> possibleDeclarationComputer,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        return IsLastTokenOfType(token, semanticModel, typeSyntaxGetter, modifierGetter, possibleDeclarationComputer, cancellationToken, out _);
    }
 
    private static NameDeclarationInfo IsLastTokenOfType<TSyntaxNode>(
        SyntaxToken token,
        SemanticModel semanticModel,
        Func<TSyntaxNode, SyntaxNode?> typeSyntaxGetter,
        Func<TSyntaxNode, SyntaxTokenList> modifierGetter,
        Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>> possibleDeclarationComputer,
        CancellationToken cancellationToken,
        out SyntaxNode? typeSyntax) where TSyntaxNode : SyntaxNode
    {
        typeSyntax = null;
        if (!IsPossibleTypeToken(token))
            return default;
 
        var target = token.GetAncestor<TSyntaxNode>();
        if (target == null)
            return default;
 
        typeSyntax = typeSyntaxGetter(target);
        if (typeSyntax == null || token != typeSyntax.GetLastToken())
            return default;
 
        var modifiers = modifierGetter(target);
 
        return new NameDeclarationInfo(
            possibleDeclarationComputer(GetDeclarationModifiers(modifiers)),
            GetAccessibility(modifiers),
            GetDeclarationModifiers(modifiers),
            semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type,
            semanticModel.GetAliasInfo(typeSyntax, cancellationToken));
    }
 
    private static bool IsFieldDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsFollowingTypeOrComma<VariableDeclarationSyntax>(token, semanticModel,
            v => v.Type,
            v => v.Parent is FieldDeclarationSyntax f ? f.Modifiers : null,
            GetPossibleMemberDeclarations,
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsIncompleteMemberDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<IncompleteMemberSyntax>(token, semanticModel,
            i => i.Type,
            i => i.Modifiers,
            GetPossibleMemberDeclarations,
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsLocalFunctionDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<LocalFunctionStatementSyntax>(token, semanticModel,
            typeSyntaxGetter: f => f.ReturnType,
            modifierGetter: f => f.Modifiers,
            possibleDeclarationComputer: GetPossibleLocalDeclarations,
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsLocalVariableDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        // If we only have a type, this can still end up being a local function (depending on the modifiers).
        var possibleDeclarationComputer = token.IsKind(SyntaxKind.CommaToken)
            ? (Func<DeclarationModifiers, ImmutableArray<SymbolKindOrTypeKind>>)
                (_ => [new SymbolKindOrTypeKind(SymbolKind.Local)])
            : GetPossibleLocalDeclarations;
 
        result = IsFollowingTypeOrComma<VariableDeclarationSyntax>(token, semanticModel,
             typeSyntaxGetter: v => v.Type,
             modifierGetter: v => v.Parent is LocalDeclarationStatementSyntax localDeclaration
                ? localDeclaration.Modifiers
                : null, // Return null to bail out.
             possibleDeclarationComputer,
             cancellationToken);
        if (result.Type != null)
        {
            return true;
        }
 
        // If the type has a trailing question mark, we may parse it as a conditional access expression.
        // We will use the condition as the type to bind; we won't make the type we bind nullable
        // because we ignore nullability when generating names anyways
        if (token.IsKind(SyntaxKind.QuestionToken) &&
            token.Parent is ConditionalExpressionSyntax conditionalExpressionSyntax)
        {
            var symbolInfo = semanticModel.GetSymbolInfo(conditionalExpressionSyntax.Condition);
 
            if (symbolInfo.GetAnySymbol() is ITypeSymbol type)
            {
                var alias = semanticModel.GetAliasInfo(conditionalExpressionSyntax.Condition, cancellationToken);
 
                result = new NameDeclarationInfo(
                    possibleDeclarationComputer(default),
                    accessibility: null,
                    type: type,
                    alias: alias);
                return true;
            }
        }
 
        return false;
    }
 
    private static bool IsEmbeddedVariableDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsFollowingTypeOrComma<VariableDeclarationSyntax>(token, semanticModel,
            typeSyntaxGetter: v => v.Type,
            modifierGetter: v => v.Parent is UsingStatementSyntax or ForStatementSyntax
                ? default(SyntaxTokenList)
                : null, // Return null to bail out.
            possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)],
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsForEachVariableDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        // This is parsed as ForEachVariableStatementSyntax:
        // foreach (int $$
        result = IsLastTokenOfType<CommonForEachStatementSyntax>(token, semanticModel,
            typeSyntaxGetter: f =>
                f is ForEachStatementSyntax forEachStatement ? forEachStatement.Type :
                f is ForEachVariableStatementSyntax forEachVariableStatement ? forEachVariableStatement.Variable :
                null, // Return null to bail out.
            modifierGetter: f => default,
            possibleDeclarationComputer: d => [new SymbolKindOrTypeKind(SymbolKind.Local)],
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsTypeParameterDeclaration(SyntaxToken token, out NameDeclarationInfo result)
    {
        if (token.Kind() is SyntaxKind.LessThanToken or SyntaxKind.CommaToken &&
            token.Parent.IsKind(SyntaxKind.TypeParameterList))
        {
            result = new NameDeclarationInfo(
                [new SymbolKindOrTypeKind(SymbolKind.TypeParameter)],
                Accessibility.NotApplicable);
 
            return true;
        }
 
        result = default;
        return false;
    }
 
    private static bool IsPrimaryConstructorParameter(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<ParameterSyntax>(
            token, semanticModel,
            p => p.Type,
            _ => default,
            _ => s_propertySyntaxKind,
            cancellationToken);
 
        if (result.Type != null &&
            token.GetAncestor<ParameterSyntax>()?.Parent?.Parent is (kind: SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration))
        {
            return true;
        }
 
        result = default;
        return false;
    }
 
    private static bool IsParameterDeclaration(SyntaxToken token, SemanticModel semanticModel,
        CancellationToken cancellationToken, out NameDeclarationInfo result)
    {
        result = IsLastTokenOfType<ParameterSyntax>(
            token, semanticModel,
            p => p.Type,
            _ => default,
            _ => s_parameterSyntaxKind,
            cancellationToken);
        return result.Type != null;
    }
 
    private static bool IsPatternMatching(
        SyntaxToken token,
        SemanticModel semanticModel,
        CancellationToken cancellationToken,
        out NameDeclarationInfo result)
    {
        result = default;
 
        var parent = token.Parent;
        if (token.Kind() is SyntaxKind.GreaterThanToken &&
            parent is TypeArgumentListSyntax { Parent: GenericNameSyntax genericName })
        {
            parent = genericName;
        }
 
        if (parent.IsParentKind(SyntaxKind.IsExpression))
        {
            result = IsLastTokenOfType<BinaryExpressionSyntax>(
                token, semanticModel,
                b => b.Right,
                _ => default,
                _ => s_parameterSyntaxKind,
                cancellationToken);
        }
        else if (parent.IsParentKind(SyntaxKind.CaseSwitchLabel))
        {
            result = IsLastTokenOfType<CaseSwitchLabelSyntax>(
                token, semanticModel,
                b => b.Value,
                _ => default,
                _ => s_parameterSyntaxKind,
                cancellationToken);
        }
        else if (parent.IsParentKind(SyntaxKind.DeclarationPattern))
        {
            result = IsLastTokenOfType<DeclarationPatternSyntax>(
                token, semanticModel,
                b => b.Type,
                _ => default,
                _ => s_parameterSyntaxKind,
                cancellationToken);
        }
 
        return result.Type != null;
    }
 
    private static bool IsPossibleTypeToken(SyntaxToken token)
        => token.Kind() is
             SyntaxKind.IdentifierToken or
             SyntaxKind.GreaterThanToken or
             SyntaxKind.CloseBracketToken or
             SyntaxKind.QuestionToken ||
           token.Parent.IsKind(SyntaxKind.PredefinedType);
 
    private static ImmutableArray<SymbolKindOrTypeKind> GetPossibleMemberDeclarations(DeclarationModifiers modifiers)
    {
        if (modifiers.IsConst || modifiers.IsReadOnly)
        {
            return [new SymbolKindOrTypeKind(SymbolKind.Field)];
        }
 
        var possibleTypes = ImmutableArray.Create(
            new SymbolKindOrTypeKind(SymbolKind.Field),
            new SymbolKindOrTypeKind(SymbolKind.Property),
            new SymbolKindOrTypeKind(MethodKind.Ordinary));
 
        if (modifiers.IsAbstract || modifiers.IsVirtual || modifiers.IsSealed || modifiers.IsOverride)
        {
            possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field));
        }
 
        if (modifiers.IsAsync || modifiers.IsPartial)
        {
            // Fields and properties cannot be async or partial.
            possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Field));
            possibleTypes = possibleTypes.Remove(new SymbolKindOrTypeKind(SymbolKind.Property));
        }
 
        return possibleTypes;
    }
 
    private static ImmutableArray<SymbolKindOrTypeKind> GetPossibleLocalDeclarations(DeclarationModifiers modifiers)
    {
        return
            modifiers.IsConst
                ? [new SymbolKindOrTypeKind(SymbolKind.Local)] :
            modifiers.IsAsync || modifiers.IsUnsafe
                ? [new SymbolKindOrTypeKind(MethodKind.LocalFunction)] :
            [new SymbolKindOrTypeKind(SymbolKind.Local), new SymbolKindOrTypeKind(MethodKind.LocalFunction)];
    }
 
    private static DeclarationModifiers GetDeclarationModifiers(SyntaxTokenList modifiers)
    {
        var declarationModifiers = new DeclarationModifiers();
        foreach (var modifer in modifiers)
        {
            switch (modifer.Kind())
            {
                case SyntaxKind.StaticKeyword:
                    declarationModifiers = declarationModifiers.WithIsStatic(true);
                    continue;
                case SyntaxKind.AbstractKeyword:
                    declarationModifiers = declarationModifiers.WithIsAbstract(true);
                    continue;
                case SyntaxKind.NewKeyword:
                    declarationModifiers = declarationModifiers.WithIsNew(true);
                    continue;
                case SyntaxKind.UnsafeKeyword:
                    declarationModifiers = declarationModifiers.WithIsUnsafe(true);
                    continue;
                case SyntaxKind.ReadOnlyKeyword:
                    declarationModifiers = declarationModifiers.WithIsReadOnly(true);
                    continue;
                case SyntaxKind.VirtualKeyword:
                    declarationModifiers = declarationModifiers.WithIsVirtual(true);
                    continue;
                case SyntaxKind.OverrideKeyword:
                    declarationModifiers = declarationModifiers.WithIsOverride(true);
                    continue;
                case SyntaxKind.SealedKeyword:
                    declarationModifiers = declarationModifiers.WithIsSealed(true);
                    continue;
                case SyntaxKind.ConstKeyword:
                    declarationModifiers = declarationModifiers.WithIsConst(true);
                    continue;
                case SyntaxKind.AsyncKeyword:
                    declarationModifiers = declarationModifiers.WithAsync(true);
                    continue;
                case SyntaxKind.PartialKeyword:
                    declarationModifiers = declarationModifiers.WithPartial(true);
                    continue;
            }
        }
 
        return declarationModifiers;
    }
 
    private static Accessibility? GetAccessibility(SyntaxTokenList modifiers)
    {
        for (var i = modifiers.Count - 1; i >= 0; i--)
        {
            var modifier = modifiers[i];
            switch (modifier.Kind())
            {
                case SyntaxKind.PrivateKeyword:
                    return Accessibility.Private;
                case SyntaxKind.PublicKeyword:
                    return Accessibility.Public;
                case SyntaxKind.ProtectedKeyword:
                    return Accessibility.Protected;
                case SyntaxKind.InternalKeyword:
                    return Accessibility.Internal;
            }
        }
 
        return null;
    }
 
    private static SyntaxNode? GetNodeDenotingTheTypeOfTupleArgument(ArgumentSyntax argumentSyntax)
    {
        switch (argumentSyntax.Expression?.Kind())
        {
            case SyntaxKind.DeclarationExpression:
                // The parser found a declaration as in (System.Action action, System.Array a$$)
                // we need the type part of the declaration expression.
                return ((DeclarationExpressionSyntax)argumentSyntax.Expression).Type;
            default:
                // We assume the parser found something that represents something named,
                // e.g. a MemberAccessExpression as in (System.Action action, System.Array $$)
                // We also assume that this name could be resolved to a type.
                return argumentSyntax.Expression;
        }
    }
 
    public static Glyph GetGlyph(SymbolKind kind, Accessibility? declaredAccessibility)
    {
        var publicIcon = kind switch
        {
            SymbolKind.Field => Glyph.FieldPublic,
            SymbolKind.Local => Glyph.Local,
            SymbolKind.Method => Glyph.MethodPublic,
            SymbolKind.Parameter => Glyph.Parameter,
            SymbolKind.Property => Glyph.PropertyPublic,
            SymbolKind.RangeVariable => Glyph.RangeVariable,
            SymbolKind.TypeParameter => Glyph.TypeParameter,
            _ => throw ExceptionUtilities.UnexpectedValue(kind),
        };
 
        switch (declaredAccessibility)
        {
            case Accessibility.Private:
                publicIcon += Glyph.ClassPrivate - Glyph.ClassPublic;
                break;
 
            case Accessibility.Protected:
            case Accessibility.ProtectedAndInternal:
            case Accessibility.ProtectedOrInternal:
                publicIcon += Glyph.ClassProtected - Glyph.ClassPublic;
                break;
 
            case Accessibility.Internal:
                publicIcon += Glyph.ClassInternal - Glyph.ClassPublic;
                break;
        }
 
        return publicIcon;
    }
 
    public static SymbolKind GetSymbolKind(SymbolKindOrTypeKind symbolKindOrTypeKind)
    {
        // There's no special glyph for local functions.
        // We don't need to differentiate them at this point.
        return symbolKindOrTypeKind.SymbolKind.HasValue ? symbolKindOrTypeKind.SymbolKind.Value :
            symbolKindOrTypeKind.MethodKind.HasValue ? SymbolKind.Method :
            throw ExceptionUtilities.Unreachable();
    }
}