File: Classification\SyntaxClassification\NameSyntaxClassifier.cs
Web Access
Project: src\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Classification.Classifiers;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Classification.Classifiers;
 
internal sealed class NameSyntaxClassifier : AbstractNameSyntaxClassifier
{
    public override void AddClassifications(
        SyntaxNode syntax,
        TextSpan textSpan,
        SemanticModel semanticModel,
        ClassificationOptions options,
        SegmentedList<ClassifiedSpan> result,
        CancellationToken cancellationToken)
    {
        if (syntax is SimpleNameSyntax name)
            ClassifyTypeSyntax(name, semanticModel, result, cancellationToken);
    }
 
    public override ImmutableArray<Type> SyntaxNodeTypes { get; } =
    [
        typeof(IdentifierNameSyntax),
        typeof(GenericNameSyntax),
    ];
 
    protected override bool IsParentAnAttribute(SyntaxNode node)
        => node.IsParentKind(SyntaxKind.Attribute);
 
    private void ClassifyTypeSyntax(
        SimpleNameSyntax name,
        SemanticModel semanticModel,
        SegmentedList<ClassifiedSpan> result,
        CancellationToken cancellationToken)
    {
        var symbolInfo = semanticModel.GetSymbolInfo(name, cancellationToken);
 
        var _ =
            TryClassifySymbol(name, symbolInfo, result) ||
            TryClassifyFromIdentifier(name, symbolInfo, result) ||
            TryClassifyValueIdentifier(name, symbolInfo, result) ||
            TryClassifySomeContextualKeywordIdentifiersAsKeywords(name, symbolInfo, result);
    }
 
    private bool TryClassifySymbol(
        SimpleNameSyntax name,
        SymbolInfo symbolInfo,
        SegmentedList<ClassifiedSpan> result)
    {
        if (symbolInfo.CandidateReason is
            CandidateReason.Ambiguous or
            CandidateReason.MemberGroup)
        {
            return TryClassifyAmbiguousSymbol(name, symbolInfo, result);
        }
 
        // Only classify if we get one good symbol back, or if it bound to a constructor symbol with
        // overload resolution/accessibility errors, or bound to type/constructor and type wasn't creatable.
        var symbol = TryGetSymbol(name, symbolInfo);
        if (TryClassifySymbol(name, symbol, out var classifiedSpan))
        {
            result.Add(classifiedSpan);
 
            if (classifiedSpan.ClassificationType != ClassificationTypeNames.Keyword)
            {
                // Additionally classify static symbols
                TryClassifyStaticSymbol(symbol, classifiedSpan.TextSpan, result);
            }
 
            return true;
        }
 
        return false;
    }
 
    private static bool TryClassifyAmbiguousSymbol(
        SimpleNameSyntax name,
        SymbolInfo symbolInfo,
        SegmentedList<ClassifiedSpan> result)
    {
        // If everything classifies the same way, then just pick that classification.
        using var _ = PooledHashSet<ClassifiedSpan>.GetInstance(out var set);
        var isStatic = false;
 
        foreach (var symbol in symbolInfo.CandidateSymbols)
        {
            if (TryClassifySymbol(name, symbol, out var classifiedSpan))
            {
                // If one symbol resolves to static, then just make it bold
                isStatic = isStatic || IsStaticSymbol(symbol);
                set.Add(classifiedSpan);
            }
        }
 
        if (set.Count == 1)
        {
            // If any of the symbols are static, add the static classification and the regular symbol classification
            if (isStatic)
            {
                result.Add(new ClassifiedSpan(set.First().TextSpan, ClassificationTypeNames.StaticSymbol));
            }
 
            result.Add(set.First());
            return true;
        }
 
        return false;
    }
 
    private static bool TryClassifySymbol(
        SimpleNameSyntax name,
        [NotNullWhen(returnValue: true)] ISymbol? symbol,
        out ClassifiedSpan classifiedSpan)
    {
        // For Namespace parts, we want don't want to classify the QualifiedNameSyntax
        // nodes, we instead wait for the each IdentifierNameSyntax node to avoid
        // creating overlapping ClassifiedSpans.
        if (symbol is INamespaceSymbol namespaceSymbol &&
            name is IdentifierNameSyntax)
        {
            // Do not classify the global:: namespace. It is already syntactically classified as a keyword.
            var isGlobalNamespace = namespaceSymbol.IsGlobalNamespace &&
                name.Identifier.IsKind(SyntaxKind.GlobalKeyword);
            if (isGlobalNamespace)
            {
                classifiedSpan = default;
                return false;
            }
 
            // Classifies both extern aliases and namespaces.
            classifiedSpan = new ClassifiedSpan(name.Span, ClassificationTypeNames.NamespaceName);
            return true;
        }
 
        if (name.IsVar && IsInVarContext(name))
        {
            // Don't do anything further to check if `var` is the contextual keyword here. We do not support code
            // squatting on typenames that are all lowercase.
            classifiedSpan = new ClassifiedSpan(name.Span, ClassificationTypeNames.Keyword);
            return true;
        }
 
        if (name is IdentifierNameSyntax { Identifier.Text: "args" } &&
            symbol is IParameterSymbol { ContainingSymbol: IMethodSymbol { Name: WellKnownMemberNames.TopLevelStatementsEntryPointMethodName } })
        {
            classifiedSpan = new ClassifiedSpan(name.Span, ClassificationTypeNames.Keyword);
            return true;
        }
 
        if (name.IsNint || name.IsNuint)
        {
            if (symbol is ITypeSymbol type && type.IsNativeIntegerType)
            {
                classifiedSpan = new ClassifiedSpan(name.Span, ClassificationTypeNames.Keyword);
                return true;
            }
        }
 
        if ((name.IsUnmanaged || name.IsNotNull) && name.Parent.IsKind(SyntaxKind.TypeConstraint))
        {
            classifiedSpan = new ClassifiedSpan(name.Span, ClassificationTypeNames.Keyword);
            return true;
        }
 
        // Use .Equals since we can't rely on object identity for constructed types.
        SyntaxToken token;
        switch (symbol)
        {
            case ITypeSymbol typeSymbol:
                var classification = GetClassificationForType(typeSymbol);
                if (classification != null)
                {
                    token = name.GetNameToken();
                    classifiedSpan = new ClassifiedSpan(token.Span, classification);
                    return true;
                }
 
                break;
 
            case IFieldSymbol fieldSymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, GetClassificationForField(fieldSymbol));
                return true;
            case IMethodSymbol methodSymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, GetClassificationForMethod(methodSymbol));
                return true;
            case IPropertySymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, ClassificationTypeNames.PropertyName);
                return true;
            case IEventSymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, ClassificationTypeNames.EventName);
                return true;
            case IParameterSymbol parameterSymbol:
                if (parameterSymbol.IsImplicitlyDeclared && parameterSymbol.Name == "value")
                {
                    break;
                }
 
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, ClassificationTypeNames.ParameterName);
                return true;
            case ILocalSymbol localSymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, GetClassificationForLocal(localSymbol));
                return true;
            case ILabelSymbol:
                token = name.GetNameToken();
                classifiedSpan = new ClassifiedSpan(token.Span, ClassificationTypeNames.LabelName);
                return true;
        }
 
        classifiedSpan = default;
        return false;
    }
 
    private static string GetClassificationForField(IFieldSymbol fieldSymbol)
    {
        if (fieldSymbol.IsConst)
        {
            return fieldSymbol.ContainingType.IsEnumType() ? ClassificationTypeNames.EnumMemberName : ClassificationTypeNames.ConstantName;
        }
 
        return ClassificationTypeNames.FieldName;
    }
 
    private static string GetClassificationForLocal(ILocalSymbol localSymbol)
    {
        return localSymbol.IsConst
            ? ClassificationTypeNames.ConstantName
            : ClassificationTypeNames.LocalName;
    }
 
    private static string GetClassificationForMethod(IMethodSymbol methodSymbol)
    {
        // Classify constructors by their containing type. We do not need to worry about
        // destructors because their declaration is handled by syntactic classification
        // and they cannot be invoked, so their is no usage to semantically classify.
        if (methodSymbol.MethodKind == MethodKind.Constructor)
        {
            return methodSymbol.ContainingType?.GetClassification() ?? ClassificationTypeNames.MethodName;
        }
 
        // Note: We only classify an extension method if it is in reduced form.
        // If an extension method is called as a static method invocation (e.g. Enumerable.Select(...)),
        // it is classified as an ordinary method.
        return methodSymbol.MethodKind == MethodKind.ReducedExtension
            ? ClassificationTypeNames.ExtensionMethodName
            : ClassificationTypeNames.MethodName;
    }
 
    private static bool IsInVarContext(SimpleNameSyntax name)
    {
        return
            name.CheckParent<RefTypeSyntax>(v => v.Type == name) ||
            name.CheckParent<ScopedTypeSyntax>(v => v.Type == name) ||
            name.CheckParent<ForEachStatementSyntax>(f => f.Type == name) ||
            name.CheckParent<DeclarationPatternSyntax>(v => v.Type == name) ||
            name.CheckParent<VariableDeclarationSyntax>(v => v.Type == name) ||
            name.CheckParent<DeclarationExpressionSyntax>(f => f.Type == name);
    }
 
    private static bool TryClassifyFromIdentifier(
        SimpleNameSyntax name,
        SymbolInfo symbolInfo,
        SegmentedList<ClassifiedSpan> result)
    {
        // Okay - it wasn't a type. If the syntax matches "var q = from" or "q = from", and from
        // doesn't bind to anything then optimistically color from as a keyword.
        if (name is IdentifierNameSyntax &&
            name.Identifier.HasMatchingText(SyntaxKind.FromKeyword) &&
            symbolInfo.Symbol == null)
        {
            var token = name.Identifier;
            if (name.IsRightSideOfAnyAssignExpression() || name.IsVariableDeclaratorValue())
            {
                result.Add(new ClassifiedSpan(token.Span, ClassificationTypeNames.Keyword));
                return true;
            }
        }
 
        return false;
    }
 
    private static bool TryClassifyValueIdentifier(
        SimpleNameSyntax name,
        SymbolInfo symbolInfo,
        SegmentedList<ClassifiedSpan> result)
    {
        if (symbolInfo.Symbol.IsImplicitValueParameter())
        {
            result.Add(new ClassifiedSpan(name.Identifier.Span, ClassificationTypeNames.Keyword));
            return true;
        }
 
        return false;
    }
 
    private static bool TryClassifySomeContextualKeywordIdentifiersAsKeywords(SimpleNameSyntax name, SymbolInfo symbolInfo, SegmentedList<ClassifiedSpan> result)
    {
        // Simple approach, if the user ever types one of identifiers from the list and it doesn't actually bind to anything, presume that
        // they intend to use it as a keyword. This works for all error
        // cases, while not conflicting with the extremely rare case where such identifiers might actually be used to
        // reference actual symbols with that names.
        if (symbolInfo.GetAnySymbol() is null &&
            name is IdentifierNameSyntax { Identifier.Text: "async" or "nameof" or "partial" })
        {
            result.Add(new(name.Span, ClassificationTypeNames.Keyword));
            return true;
        }
 
        return false;
    }
}