File: Progression\GraphProvider.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.NavigateTo;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.GraphModel;
using Microsoft.VisualStudio.GraphModel.CodeSchema;
using Microsoft.VisualStudio.GraphModel.Schemas;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Progression;
using Microsoft.VisualStudio.Shell;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Progression;
 
[GraphProvider(Name = nameof(RoslynGraphProvider), ProjectCapability = "(CSharp | VB)")]
internal sealed class RoslynGraphProvider : IGraphProvider
{
    private readonly IThreadingContext _threadingContext;
    private readonly IGlyphService _glyphService;
    private readonly IServiceProvider _serviceProvider;
    private readonly IAsynchronousOperationListener _asyncListener;
    private readonly Workspace _workspace;
    private readonly Lazy<IStreamingFindUsagesPresenter> _streamingPresenter;
    private readonly GraphQueryManager _graphQueryManager;
 
    private bool _initialized = false;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public RoslynGraphProvider(
        IThreadingContext threadingContext,
        IGlyphService glyphService,
        SVsServiceProvider serviceProvider,
        VisualStudioWorkspace workspace,
        Lazy<IStreamingFindUsagesPresenter> streamingPresenter,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        _threadingContext = threadingContext;
        _glyphService = glyphService;
        _serviceProvider = serviceProvider;
        _asyncListener = listenerProvider.GetListener(FeatureAttribute.GraphProvider);
        _workspace = workspace;
        _streamingPresenter = streamingPresenter;
        _graphQueryManager = new GraphQueryManager(workspace, threadingContext, _asyncListener);
    }
 
    private void EnsureInitialized()
    {
        if (_initialized)
        {
            return;
        }
 
        var iconService = (IIconService)_serviceProvider.GetService(typeof(IIconService));
        IconHelper.Initialize(_glyphService, iconService);
        _initialized = true;
    }
 
    public static ImmutableArray<IGraphQuery> GetGraphQueries(IGraphContext context)
    {
        using var _ = ArrayBuilder<IGraphQuery>.GetInstance(out var graphQueries);
 
        if (context.Direction == GraphContextDirection.Self && context.RequestedProperties.Contains(DgmlNodeProperties.ContainsChildren))
        {
            graphQueries.Add(new ContainsChildrenGraphQuery());
        }
 
        if (context.Direction == GraphContextDirection.Contains ||
            (context.Direction == GraphContextDirection.Target && context.LinkCategories.Contains(CodeLinkCategories.Contains)))
        {
            graphQueries.Add(new ContainsGraphQuery());
        }
 
        if (context.LinkCategories.Contains(CodeLinkCategories.InheritsFrom))
        {
            if (context.Direction == GraphContextDirection.Target)
            {
                graphQueries.Add(new InheritsGraphQuery());
            }
            else if (context.Direction == GraphContextDirection.Source)
            {
                graphQueries.Add(new InheritedByGraphQuery());
            }
        }
 
        if (context.LinkCategories.Contains(CodeLinkCategories.SourceReferences))
        {
            graphQueries.Add(new IsUsedByGraphQuery());
        }
 
        if (context.LinkCategories.Contains(CodeLinkCategories.Calls))
        {
            if (context.Direction == GraphContextDirection.Target)
            {
                graphQueries.Add(new CallsGraphQuery());
            }
            else if (context.Direction == GraphContextDirection.Source)
            {
                graphQueries.Add(new IsCalledByGraphQuery());
            }
        }
 
        if (context.LinkCategories.Contains(CodeLinkCategories.Implements))
        {
            if (context.Direction == GraphContextDirection.Target)
            {
                graphQueries.Add(new ImplementsGraphQuery());
            }
            else if (context.Direction == GraphContextDirection.Source)
            {
                graphQueries.Add(new ImplementedByGraphQuery());
            }
        }
 
        if (context.LinkCategories.Contains(RoslynGraphCategories.Overrides))
        {
            if (context.Direction == GraphContextDirection.Source)
            {
                graphQueries.Add(new OverridesGraphQuery());
            }
            else if (context.Direction == GraphContextDirection.Target)
            {
                graphQueries.Add(new OverriddenByGraphQuery());
            }
        }
 
        if (context.Direction == GraphContextDirection.Custom)
        {
            var searchParameters = context.GetValue<ISolutionSearchParameters>(typeof(ISolutionSearchParameters).GUID.ToString());
 
            if (searchParameters != null)
            {
                // WARNING: searchParameters.SearchQuery returns an IVsSearchQuery object, which is a COM type.
                // Therefore, it's probably best to grab the values we want now rather than get surprised by COM
                // marshalling later.
                //
                // Create two queries.  One to find results in normal docs, and one to find results in generated
                // docs.  That way if the generated docs take a long time we can still report the regular doc
                // results immediately.
                graphQueries.Add(new SearchGraphQuery(searchParameters.SearchQuery.SearchString, NavigateToDocumentSupport.RegularDocuments));
                graphQueries.Add(new SearchGraphQuery(searchParameters.SearchQuery.SearchString, NavigateToDocumentSupport.GeneratedDocuments));
            }
        }
 
        return graphQueries.ToImmutableAndClear();
    }
 
    public void BeginGetGraphData(IGraphContext context)
    {
        EnsureInitialized();
 
        var graphQueries = GetGraphQueries(context);
 
        // Perform the queries asynchronously  in a fire-and-forget fashion.  This helper will be responsible
        // for always completing the context. AddQueriesAsync is `async`, so it always returns a task and will never
        // bubble out an exception synchronously (so CompletesAsyncOperation is safe).
        var asyncToken = _asyncListener.BeginAsyncOperation(nameof(BeginGetGraphData));
        _ = _graphQueryManager
            .AddQueriesAsync(context, graphQueries, _threadingContext.DisposalToken)
            .CompletesAsyncOperation(asyncToken);
    }
 
    public IEnumerable<GraphCommand> GetCommands(IEnumerable<GraphNode> nodes)
    {
        EnsureInitialized();
 
        // Only nodes that explicitly state that they contain children (e.g., source files) and named types should
        // be expandable.
        if (nodes.Any(n => n.Properties.Any(p => p.Key == DgmlNodeProperties.ContainsChildren)) ||
            nodes.Any(n => IsAnySymbolKind(n, SymbolKind.NamedType)))
        {
            yield return new GraphCommand(
                GraphCommandDefinition.Contains,
                targetCategories: null,
                linkCategories: [GraphCommonSchema.Contains],
                trackChanges: true);
        }
 
        // All graph commands below this point apply only to Roslyn-owned nodes.
        if (!nodes.All(n => IsRoslynNode(n)))
        {
            yield break;
        }
 
        // Only show 'Base Types' and 'Derived Types' on a class or interface.
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.NamedType) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Interface, TypeKind.Struct, TypeKind.Enum, TypeKind.Delegate)))
        {
            yield return new GraphCommand(
                GraphCommandDefinition.BaseTypes,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.InheritsFrom],
                trackChanges: true);
 
            yield return new GraphCommand(
                GraphCommandDefinition.DerivedTypes,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.InheritsFrom],
                trackChanges: true);
        }
 
        // Only show 'Calls' on an applicable member in a class or struct
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property, SymbolKind.Field)))
        {
            yield return new GraphCommand(
                GraphCommandDefinition.Calls,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.Calls],
                trackChanges: true);
        }
 
        // Only show 'Is Called By' on an applicable member in a class or struct
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Struct)))
        {
            yield return new GraphCommand(
                GraphCommandDefinition.IsCalledBy,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.Calls],
                trackChanges: true);
        }
 
        // Show 'Is Used By'
        yield return new GraphCommand(
            GraphCommandDefinition.IsUsedBy,
            targetCategories: [CodeNodeCategories.SourceLocation],
            linkCategories: [CodeLinkCategories.SourceReferences],
            trackChanges: true);
 
        // Show 'Implements' on a class or struct, or an applicable member in a class or struct.
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.NamedType) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Struct)))
        {
            yield return new GraphCommand(
                s_implementsCommandDefinition,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.Implements],
                trackChanges: true);
        }
 
        // Show 'Implements' on public, non-static members of a class or struct.  Note: we should
        // also show it on explicit interface impls in C#.
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Struct) &&
                           !GetModifiers(n).IsStatic))
        {
            if (nodes.Any(n => CheckAccessibility(n, Accessibility.Public) ||
                               HasExplicitInterfaces(n)))
            {
                yield return new GraphCommand(
                    s_implementsCommandDefinition,
                    targetCategories: null,
                    linkCategories: [CodeLinkCategories.Implements],
                    trackChanges: true);
            }
        }
 
        // Show 'Implemented By' on an interface.
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.NamedType) &&
                           IsAnyTypeKind(n, TypeKind.Interface)))
        {
            yield return new GraphCommand(
                s_implementedByCommandDefinition,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.Implements],
                trackChanges: true);
        }
 
        // Show 'Implemented By' on any member of an interface.
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property) &&
                           IsAnyTypeKind(n, TypeKind.Interface)))
        {
            yield return new GraphCommand(
                s_implementedByCommandDefinition,
                targetCategories: null,
                linkCategories: [CodeLinkCategories.Implements],
                trackChanges: true);
        }
 
        // Show 'Overrides' on any applicable member of a class or struct
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Struct) &&
                           GetModifiers(n).IsOverride))
        {
            yield return new GraphCommand(
                s_overridesCommandDefinition,
                targetCategories: null,
                linkCategories: [RoslynGraphCategories.Overrides],
                trackChanges: true);
        }
 
        // Show 'Overridden By' on any applicable member of a class or struct
        if (nodes.Any(n => IsAnySymbolKind(n, SymbolKind.Event, SymbolKind.Method, SymbolKind.Property) &&
                           IsAnyTypeKind(n, TypeKind.Class, TypeKind.Struct) &&
                           IsOverridable(n)))
        {
            yield return new GraphCommand(
                s_overriddenByCommandDefinition,
                targetCategories: null,
                linkCategories: [RoslynGraphCategories.Overrides],
                trackChanges: true);
        }
    }
 
    private static bool IsOverridable(GraphNode node)
    {
        var modifiers = GetModifiers(node);
        return (modifiers.IsVirtual || modifiers.IsAbstract || modifiers.IsOverride) &&
            !modifiers.IsSealed;
    }
 
    private static DeclarationModifiers GetModifiers(GraphNode node)
        => (DeclarationModifiers)node[RoslynGraphProperties.SymbolModifiers];
 
    private static bool CheckAccessibility(GraphNode node, Accessibility accessibility)
        => node[RoslynGraphProperties.DeclaredAccessibility].Equals(accessibility);
 
    private static bool HasExplicitInterfaces(GraphNode node)
        => ((IList<SymbolKey>)node[RoslynGraphProperties.ExplicitInterfaceImplementations]).Count > 0;
 
    private static bool IsRoslynNode(GraphNode node)
    {
        return node[RoslynGraphProperties.SymbolKind] != null
            && node[RoslynGraphProperties.TypeKind] != null;
    }
 
    private static bool IsAnySymbolKind(GraphNode node, params SymbolKind[] symbolKinds)
        => symbolKinds.Any(k => k.Equals(node[RoslynGraphProperties.SymbolKind]));
 
    private static bool IsAnyTypeKind(GraphNode node, params TypeKind[] typeKinds)
        => typeKinds.Any(k => node[RoslynGraphProperties.TypeKind].Equals(k));
 
    private static readonly GraphCommandDefinition s_overridesCommandDefinition =
        new("Overrides", EditorFeaturesResources.Overrides_, GraphContextDirection.Target, 700);
 
    private static readonly GraphCommandDefinition s_overriddenByCommandDefinition =
        new("OverriddenBy", EditorFeaturesResources.Overridden_By, GraphContextDirection.Source, 700);
 
    private static readonly GraphCommandDefinition s_implementsCommandDefinition =
        new("Implements", EditorFeaturesResources.Implements_, GraphContextDirection.Target, 600);
 
    private static readonly GraphCommandDefinition s_implementedByCommandDefinition =
        new("ImplementedBy", EditorFeaturesResources.Implemented_By, GraphContextDirection.Source, 600);
 
    public T? GetExtension<T>(GraphObject graphObject, T previous) where T : class
    {
        if (graphObject is GraphNode graphNode)
        {
            // If this is not a Roslyn node, bail out.
            if (graphNode.GetValue(RoslynGraphProperties.ContextProjectId) == null)
                return null;
 
            // Has to have at least a symbolid, or source location to navigate to.
            if (graphNode.GetValue<SymbolKey?>(RoslynGraphProperties.SymbolId) == null &&
                graphNode.GetValue<SourceLocation>(CodeNodeProperties.SourceLocation).FileName == null)
            {
                return null;
            }
 
            if (typeof(T) == typeof(IGraphNavigateToItem))
                return new GraphNavigatorExtension(_threadingContext, _workspace, _streamingPresenter) as T;
 
            if (typeof(T) == typeof(IGraphFormattedLabel))
                return new GraphFormattedLabelExtension() as T;
        }
 
        return null;
    }
 
    public Graph? Schema
    {
        get { return null; }
    }
}