File: ObsoleteSymbol\AbstractObsoleteSymbolService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.ObsoleteSymbol;
 
internal abstract class AbstractObsoleteSymbolService(int? dimKeywordKind) : IObsoleteSymbolService
{
    /// <summary>
    /// The <see cref="SyntaxToken.RawKind"/> of the <see langword="Dim"/> keyword in Visual Basic, or
    /// <see langword="null"/> for C# scenarios. This value is used to improve performance in the token classification
    /// fast-path by avoiding unnecessary calls to <see cref="ProcessDimKeyword"/>.
    /// </summary>
    private readonly int? _dimKeywordKind = dimKeywordKind;
 
    protected virtual void ProcessDimKeyword(ref ArrayBuilder<TextSpan>? result, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken)
    {
        // Take no action by default
    }
 
    public async Task<ImmutableArray<TextSpan>> GetLocationsAsync(Document document, ImmutableArray<TextSpan> textSpans, CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        // Obsolete analysis doesn't need nullable information.  This saves substantial time computing obsoletion information.
        var semanticModel = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        // Avoid taking a builder from the pool in the common case where there are no references to obsolete symbols
        // currently on screen.
        ArrayBuilder<TextSpan>? result = null;
        try
        {
            foreach (var span in textSpans)
            {
                Recurse(span, semanticModel);
            }
 
            if (result is null)
                return ImmutableArray<TextSpan>.Empty;
 
            result.RemoveDuplicates();
            return result.ToImmutableAndClear();
        }
        finally
        {
            result?.Free();
        }
 
        void Recurse(TextSpan span, SemanticModel semanticModel)
        {
            using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var stack);
 
            // Walk through all the nodes in the provided span.  Directly analyze local or parameter declaration.  And
            // also analyze any identifiers which might be reference to locals or parameters.  Note that we might hit
            // locals/parameters without any references in the span, or references that don't have the declarations in 
            // the span
            stack.Add(root.FindNode(span));
 
            // Use a stack so we don't blow out the stack with recursion.
            while (stack.TryPop(out var current))
            {
                if (current.Span.IntersectsWith(span))
                {
                    var tokenFromNode = ProcessNode(semanticModel, current);
 
                    foreach (var child in current.ChildNodesAndTokens())
                    {
                        if (child.AsNode(out var childNode))
                            stack.Add(childNode);
 
                        var token = child.AsToken();
                        if (token != tokenFromNode)
                            ProcessToken(semanticModel, child.AsToken());
 
                        ExtractStructureFromTrivia(stack, token.LeadingTrivia);
                        ExtractStructureFromTrivia(stack, token.TrailingTrivia);
                    }
                }
            }
        }
 
        static void ExtractStructureFromTrivia(ArrayBuilder<SyntaxNode> stack, SyntaxTriviaList triviaList)
        {
            foreach (var trivia in triviaList)
            {
                if (trivia.HasStructure)
                {
                    stack.Add(trivia.GetStructure()!);
                }
            }
        }
 
        void AddResult(TextSpan span)
        {
            result ??= ArrayBuilder<TextSpan>.GetInstance();
            result.Add(span);
        }
 
        SyntaxToken ProcessNode(SemanticModel semanticModel, SyntaxNode node)
        {
            if (syntaxFacts.IsUsingAliasDirective(node))
            {
                syntaxFacts.GetPartsOfUsingAliasDirective(node, out _, out var aliasToken, out var name);
                if (!aliasToken.Span.IsEmpty)
                {
                    // Use 'name.Parent' because VB can't resolve the declared symbol directly from 'node'
                    var symbol = semanticModel.GetDeclaredSymbol(name.GetRequiredParent(), cancellationToken);
                    if (IsSymbolObsolete(symbol))
                        AddResult(aliasToken.Span);
                }
 
                return aliasToken;
            }
            else if (syntaxFacts.IsObjectCreationExpression(node))
            {
                syntaxFacts.GetPartsOfObjectCreationExpression(node, out var creationKeyword, out _, out _, out _);
                if (!creationKeyword.Span.IsEmpty)
                {
                    // For syntax like the following
                    //
                    //   SomeType value = new SomeType();
                    //
                    // We classify 'new' as obsolete only if the specific constructor is obsolete. If the containing
                    // type is obsolete, the classification will be applied to 'SomeType' instead.
                    var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol;
                    if (IsSymbolObsolete(symbol))
                        AddResult(creationKeyword.Span);
                }
            }
            else if (syntaxFacts.IsImplicitObjectCreationExpression(node))
            {
                syntaxFacts.GetPartsOfImplicitObjectCreationExpression(node, out var creationKeyword, out _, out _);
                if (!creationKeyword.Span.IsEmpty)
                {
                    // For syntax like the following
                    //
                    //   SomeType value = new();
                    //
                    // We classify 'new' as obsolete if either the type or the specific constructor is obsolete.
                    var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol;
                    if (IsSymbolObsolete(symbol) || IsSymbolObsolete(symbol?.ContainingType))
                        AddResult(creationKeyword.Span);
                }
            }
 
            return default;
        }
 
        void ProcessToken(SemanticModel semanticModel, SyntaxToken token)
        {
            if (syntaxFacts.IsIdentifier(token))
            {
                ProcessIdentifier(semanticModel, token);
            }
            else if (token.RawKind == _dimKeywordKind)
            {
                ProcessDimKeyword(ref result, semanticModel, token, cancellationToken);
            }
        }
 
        void ProcessIdentifier(SemanticModel semanticModel, SyntaxToken token)
        {
            if (syntaxFacts.IsDeclaration(token.Parent))
            {
                var symbol = semanticModel.GetDeclaredSymbol(token.Parent, cancellationToken);
                if (IsSymbolObsolete(symbol))
                    AddResult(token.Span);
            }
            else
            {
                var symbol = semanticModel.GetSymbolInfo(token, cancellationToken).Symbol;
                if (IsSymbolObsolete(symbol))
                    AddResult(token.Span);
            }
        }
    }
 
    protected static bool IsSymbolObsolete([NotNullWhen(true)] ISymbol? symbol)
    {
        // Avoid infinite recursion. Iteration limit chosen arbitrarily; cases are generally expected to complete on
        // the first iteration or fail completely.
        for (var i = 0; i < 5; i++)
        {
            if (symbol is IAliasSymbol alias)
            {
                symbol = alias.Target;
                continue;
            }
 
            if (symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments: [var valueType] })
            {
                symbol = valueType;
                continue;
            }
 
            return symbol?.IsObsolete() ?? false;
        }
 
        // Unable to determine whether the symbol is considered obsolete
        return false;
    }
}