File: Progression\GraphBuilder.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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.NavigateTo;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.GraphModel;
using Microsoft.VisualStudio.GraphModel.CodeSchema;
using Microsoft.VisualStudio.GraphModel.Schemas;
using Microsoft.VisualStudio.Progression.CodeSchema;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Progression;
 
internal sealed partial class GraphBuilder
{
    // Our usage of SemaphoreSlim is fine.  We don't perform blocking waits for it on the UI thread.
#pragma warning disable RS0030 // Do not use banned APIs
    private readonly SemaphoreSlim _gate = new(initialCount: 1);
#pragma warning restore RS0030 // Do not use banned APIs
 
    private readonly ISet<GraphNode> _createdNodes = new HashSet<GraphNode>();
    private readonly IList<Tuple<GraphNode, GraphProperty, object>> _deferredPropertySets = new List<Tuple<GraphNode, GraphProperty, object>>();
 
    private readonly Dictionary<GraphNode, Project> _nodeToContextProjectMap = [];
    private readonly Dictionary<GraphNode, Document> _nodeToContextDocumentMap = [];
    private readonly Dictionary<GraphNode, ISymbol> _nodeToSymbolMap = [];
 
    /// <summary>
    /// The input solution. Never null.
    /// </summary>
    private readonly Solution _solution;
 
    public GraphBuilder(Solution solution)
    {
        _solution = solution;
    }
 
    public static async Task<GraphBuilder> CreateForInputNodesAsync(
        Solution solution, IEnumerable<GraphNode> inputNodes, CancellationToken cancellationToken)
    {
        var builder = new GraphBuilder(solution);
 
        foreach (var inputNode in inputNodes)
        {
            if (inputNode.HasCategory(CodeNodeCategories.File))
            {
                builder.PopulateMapsForFileInputNode(inputNode, cancellationToken);
            }
            else if (!inputNode.HasCategory(CodeNodeCategories.SourceLocation))
            {
                await builder.PopulateMapsForSymbolInputNodeAsync(inputNode, cancellationToken).ConfigureAwait(false);
            }
        }
 
        return builder;
    }
 
    private void PopulateMapsForFileInputNode(GraphNode inputNode, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            var projectPath = inputNode.Id.GetNestedValueByName<Uri>(CodeGraphNodeIdName.Assembly);
            var filePath = inputNode.Id.GetNestedValueByName<Uri>(CodeGraphNodeIdName.File);
 
            if (projectPath == null || filePath == null)
            {
                return;
            }
 
            var docIdsWithPath = _solution.GetDocumentIdsWithFilePath(filePath.OriginalString);
            Document? document = null;
            Project? project = null;
 
            foreach (var docIdWithPath in docIdsWithPath)
            {
                var projectState = _solution.GetProjectState(docIdWithPath.ProjectId);
                if (projectState == null)
                {
                    FatalError.ReportAndCatch(new Exception("GetDocumentIdsWithFilePath returned a document in a project that does not exist."));
                    continue;
                }
 
                if (string.Equals(projectState.FilePath, projectPath.OriginalString))
                {
                    project = _solution.GetRequiredProject(projectState.Id);
                    document = project.GetDocument(docIdWithPath);
                    break;
                }
            }
 
            if (document == null || project == null)
            {
                return;
            }
 
            _nodeToContextProjectMap.Add(inputNode, project);
            _nodeToContextDocumentMap.Add(inputNode, document);
        }
    }
 
    private async Task PopulateMapsForSymbolInputNodeAsync(GraphNode inputNode, CancellationToken cancellationToken)
    {
        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            var projectId = (ProjectId)inputNode[RoslynGraphProperties.ContextProjectId];
            if (projectId == null)
            {
                return;
            }
 
            var project = _solution.GetProject(projectId);
            if (project == null)
            {
                return;
            }
 
            _nodeToContextProjectMap.Add(inputNode, project);
 
            var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
            var symbolId = (SymbolKey?)inputNode[RoslynGraphProperties.SymbolId];
            var symbol = symbolId.Value.Resolve(compilation, cancellationToken: cancellationToken).Symbol;
            if (symbol != null)
            {
                _nodeToSymbolMap.Add(inputNode, symbol);
            }
 
            var documentId = (DocumentId)inputNode[RoslynGraphProperties.ContextDocumentId];
            if (documentId != null)
            {
                var document = project.GetDocument(documentId);
                if (document != null)
                {
                    _nodeToContextDocumentMap.Add(inputNode, document);
                }
            }
        }
    }
 
    public Project GetContextProject(GraphNode node, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            _nodeToContextProjectMap.TryGetValue(node, out var project);
            return project;
        }
    }
 
    public ProjectId GetContextProjectId(Project project, ISymbol symbol)
    {
        var thisProject = project.Solution.GetProject(symbol.ContainingAssembly) ?? project;
        return thisProject.Id;
    }
 
    public Document GetContextDocument(GraphNode node, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            _nodeToContextDocumentMap.TryGetValue(node, out var document);
            return document;
        }
    }
 
    public ISymbol GetSymbol(GraphNode node, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            _nodeToSymbolMap.TryGetValue(node, out var symbol);
            return symbol;
        }
    }
 
    public Task<GraphNode> AddNodeAsync(ISymbol symbol, GraphNode relatedNode, CancellationToken cancellationToken)
    {
        // The lack of a lock here is acceptable, since each of the functions lock, and GetContextProject/GetContextDocument
        // never change for the same input.
        return AddNodeAsync(
            symbol,
            GetContextProject(relatedNode, cancellationToken),
            GetContextDocument(relatedNode, cancellationToken),
            cancellationToken);
    }
 
    public async Task<GraphNode> AddNodeAsync(
        ISymbol symbol, Project contextProject, Document contextDocument, CancellationToken cancellationToken)
    {
        // Figure out what the location for this node should be. We'll arbitrarily pick the
        // first one, unless we have a contextDocument to restrict it
        var preferredLocation = symbol.Locations.FirstOrDefault(l => l.SourceTree != null);
 
        if (contextDocument != null)
        {
            var syntaxTree = await contextDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
            // If we have one in that tree, use it
            preferredLocation = symbol.Locations.FirstOrDefault(l => l.SourceTree == syntaxTree) ?? preferredLocation;
        }
 
        // We may need to look up source code within this solution
        if (preferredLocation == null && symbol.Locations.Any(static loc => loc.IsInMetadata))
        {
            var newSymbol = SymbolFinder.FindSourceDefinition(symbol, contextProject.Solution, cancellationToken);
            if (newSymbol != null)
                preferredLocation = newSymbol.Locations.Where(loc => loc.IsInSource).FirstOrDefault();
        }
 
        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            var node = await GetOrCreateNodeAsync(Graph, symbol, _solution, cancellationToken).ConfigureAwait(false);
 
            node[RoslynGraphProperties.SymbolId] = (SymbolKey?)symbol.GetSymbolKey(cancellationToken);
            node[RoslynGraphProperties.ContextProjectId] = GetContextProjectId(contextProject, symbol);
            node[RoslynGraphProperties.ExplicitInterfaceImplementations] = symbol.ExplicitInterfaceImplementations().Select(s => s.GetSymbolKey()).ToList();
            node[RoslynGraphProperties.DeclaredAccessibility] = symbol.DeclaredAccessibility;
            node[RoslynGraphProperties.SymbolModifiers] = symbol.GetSymbolModifiers();
            node[RoslynGraphProperties.SymbolKind] = symbol.Kind;
 
            if (contextDocument != null)
                node[RoslynGraphProperties.ContextDocumentId] = contextDocument.Id;
 
            if (preferredLocation?.SourceTree != null)
            {
                var lineSpan = preferredLocation.GetLineSpan();
                var sourceLocation = TryCreateSourceLocation(
                    preferredLocation.SourceTree.FilePath,
                    lineSpan.Span);
                if (sourceLocation != null)
                    node[CodeNodeProperties.SourceLocation] = sourceLocation.Value;
            }
 
            // Keep track of this as a node we have added. Note this is a HashSet, so if the node was already added
            // we won't double-count.
            _createdNodes.Add(node);
 
            _nodeToSymbolMap[node] = symbol;
            _nodeToContextProjectMap[node] = contextProject;
 
            if (contextDocument != null)
                _nodeToContextDocumentMap[node] = contextDocument;
 
            return node;
        }
    }
 
    internal static async Task<GraphNode> GetOrCreateNodeAsync(Graph graph, ISymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        GraphNode node;
 
        switch (symbol.Kind)
        {
            case SymbolKind.Assembly:
                node = await GetOrCreateNodeAssemblyAsync(graph, (IAssemblySymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Namespace:
                node = await GetOrCreateNodeForNamespaceAsync(graph, (INamespaceSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.NamedType:
            case SymbolKind.ErrorType:
                node = await GetOrCreateNodeForNamedTypeAsync(graph, (INamedTypeSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Method:
                node = await GetOrCreateNodeForMethodAsync(graph, (IMethodSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Field:
                node = await GetOrCreateNodeForFieldAsync(graph, (IFieldSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Property:
                node = await GetOrCreateNodeForPropertyAsync(graph, (IPropertySymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Event:
                node = await GetOrCreateNodeForEventAsync(graph, (IEventSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Parameter:
                node = await GetOrCreateNodeForParameterAsync(graph, (IParameterSymbol)symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            case SymbolKind.Local:
            case SymbolKind.RangeVariable:
                node = await GetOrCreateNodeForLocalVariableAsync(graph, symbol, solution, cancellationToken).ConfigureAwait(false);
                break;
 
            default:
                throw new ArgumentException("symbol");
        }
 
        UpdatePropertiesForNode(symbol, node);
        UpdateLabelsForNode(symbol, solution, node);
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForParameterAsync(Graph graph, IParameterSymbol parameterSymbol, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForParameterAsync(parameterSymbol, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
        node.AddCategory(CodeNodeCategories.Parameter);
 
        node.SetValue<bool>(Properties.IsByReference, parameterSymbol.RefKind == RefKind.Ref);
        node.SetValue<bool>(Properties.IsOut, parameterSymbol.RefKind == RefKind.Out);
        node.SetValue<bool>(Properties.IsParameterArray, parameterSymbol.IsParams);
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForLocalVariableAsync(Graph graph, ISymbol localSymbol, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForLocalVariableAsync(localSymbol, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
        node.AddCategory(NodeCategories.LocalExpression);
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeAssemblyAsync(Graph graph, IAssemblySymbol assemblySymbol, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForAssemblyAsync(assemblySymbol, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
        node.AddCategory(CodeNodeCategories.Assembly);
 
        return node;
    }
 
    private static void UpdateLabelsForNode(ISymbol symbol, Solution solution, GraphNode node)
    {
        var progressionLanguageService = solution.Services.GetLanguageServices(symbol.Language).GetService<IProgressionLanguageService>();
 
        // A call from unittest may not have a proper language service.
        if (progressionLanguageService != null)
        {
            node[RoslynGraphProperties.Description] = progressionLanguageService.GetDescriptionForSymbol(symbol, includeContainingSymbol: false);
            node[RoslynGraphProperties.DescriptionWithContainingSymbol] = progressionLanguageService.GetDescriptionForSymbol(symbol, includeContainingSymbol: true);
 
            node[RoslynGraphProperties.FormattedLabelWithoutContainingSymbol] = progressionLanguageService.GetLabelForSymbol(symbol, includeContainingSymbol: false);
            node[RoslynGraphProperties.FormattedLabelWithContainingSymbol] = progressionLanguageService.GetLabelForSymbol(symbol, includeContainingSymbol: true);
        }
 
        switch (symbol.Kind)
        {
            case SymbolKind.NamedType:
                var typeSymbol = (INamedTypeSymbol)symbol;
                if (typeSymbol.IsGenericType)
                {
                    // Symbol.name does not contain type params for generic types, so we populate them here for some requiring cases like VS properties panel.
                    node.Label = (string)node[RoslynGraphProperties.FormattedLabelWithoutContainingSymbol];
 
                    // Some consumers like CodeMap want to show types in an unified way for both C# and VB.
                    // Therefore, populate a common label property using only name and its type parameters.
                    // For example, VB's "Goo(Of T)" or C#'s "Goo<T>(): T" will be shown as "Goo<T>".
                    // This property will be used for drag-and-drop case.
                    var commonLabel = new System.Text.StringBuilder();
                    commonLabel.Append(typeSymbol.Name);
                    commonLabel.Append('<');
                    commonLabel.Append(string.Join(", ", typeSymbol.TypeParameters.Select(t => t.Name)));
                    commonLabel.Append('>');
                    node[Microsoft.VisualStudio.ArchitectureTools.ProgressiveReveal.ProgressiveRevealSchema.CommonLabel] = commonLabel.ToString();
 
                    return;
                }
                else
                {
                    node.Label = symbol.Name;
                }
 
                break;
 
            case SymbolKind.Method:
                var methodSymbol = (IMethodSymbol)symbol;
                if (methodSymbol.MethodKind == MethodKind.Constructor)
                {
                    node.Label = CodeQualifiedIdentifierBuilder.SpecialNames.GetConstructorLabel(methodSymbol.ContainingSymbol.Name);
                }
                else if (methodSymbol.MethodKind == MethodKind.StaticConstructor)
                {
                    node.Label = CodeQualifiedIdentifierBuilder.SpecialNames.GetStaticConstructorLabel(methodSymbol.ContainingSymbol.Name);
                }
                else if (methodSymbol.MethodKind == MethodKind.Destructor)
                {
                    node.Label = CodeQualifiedIdentifierBuilder.SpecialNames.GetFinalizerLabel(methodSymbol.ContainingSymbol.Name);
                }
                else
                {
                    node.Label = methodSymbol.Name;
                }
 
                break;
 
            case SymbolKind.Property:
                node.Label = symbol.MetadataName;
 
                var propertySymbol = (IPropertySymbol)symbol;
                if (propertySymbol.IsIndexer && LanguageNames.CSharp == propertySymbol.Language)
                {
                    // For C# indexer, we will strip off the "[]"
                    node.Label = symbol.Name.Replace("[]", string.Empty);
                }
 
                break;
 
            case SymbolKind.Namespace:
                // Use a name with its parents (e.g., A.B.C)
                node.Label = symbol.ToDisplayString();
                break;
 
            default:
                node.Label = symbol.Name;
                break;
        }
 
        // When a node is dragged and dropped from SE to CodeMap, its label could be reset during copying to clipboard.
        // So, we try to keep its label that we computed above in a common label property, which CodeMap can access later.
        node[Microsoft.VisualStudio.ArchitectureTools.ProgressiveReveal.ProgressiveRevealSchema.CommonLabel] = node.Label;
    }
 
    private static void UpdatePropertiesForNode(ISymbol symbol, GraphNode node)
    {
        // Set accessibility properties
        switch (symbol.DeclaredAccessibility)
        {
            case Accessibility.Public:
                node[Properties.IsPublic] = true;
                break;
 
            case Accessibility.Internal:
                node[Properties.IsInternal] = true;
                break;
 
            case Accessibility.Protected:
                node[Properties.IsProtected] = true;
                break;
 
            case Accessibility.Private:
                node[Properties.IsPrivate] = true;
                break;
 
            case Accessibility.ProtectedOrInternal:
                node[Properties.IsProtectedOrInternal] = true;
                break;
 
            case Accessibility.ProtectedAndInternal:
                node[Properties.IsProtected] = true;
                node[Properties.IsInternal] = true;
                break;
 
            case Accessibility.NotApplicable:
                break;
        }
 
        // Set common properties
        if (symbol.IsAbstract)
        {
            node[Properties.IsAbstract] = true;
        }
 
        if (symbol.IsSealed)
        {
            // For VB module, do not set IsFinal since it's not inheritable.
            if (!symbol.IsModuleType())
            {
                node[Properties.IsFinal] = true;
            }
        }
 
        if (symbol.IsStatic)
        {
            node[Properties.IsStatic] = true;
        }
 
        if (symbol.IsVirtual)
        {
            node[Properties.IsVirtual] = true;
        }
 
        if (symbol.IsOverride)
        {
            // The property name is a misnomer, but this is what the previous providers do.
            node[Microsoft.VisualStudio.Progression.DgmlProperties.IsOverloaded] = true;
        }
 
        // Set type-specific properties
        if (symbol is ITypeSymbol typeSymbol && typeSymbol.IsAnonymousType)
        {
            node[Properties.IsAnonymous] = true;
        }
        else if (symbol is IMethodSymbol methodSymbol)
        {
            UpdateMethodPropertiesForNode(methodSymbol, node);
        }
    }
 
    private static void UpdateMethodPropertiesForNode(IMethodSymbol symbol, GraphNode node)
    {
        if (symbol.HidesBaseMethodsByName)
        {
            node[Properties.IsHideBySignature] = true;
        }
 
        if (symbol.IsExtensionMethod)
        {
            node[Properties.IsExtension] = true;
        }
 
        switch (symbol.MethodKind)
        {
            case MethodKind.AnonymousFunction:
                node[Properties.IsAnonymous] = true;
                break;
 
            case MethodKind.BuiltinOperator:
            case MethodKind.UserDefinedOperator:
                node[Properties.IsOperator] = true;
                break;
 
            case MethodKind.Constructor:
            case MethodKind.StaticConstructor:
                node[Properties.IsConstructor] = true;
                break;
 
            case MethodKind.Conversion:
                // Operator implicit/explicit
                node[Properties.IsOperator] = true;
                break;
 
            case MethodKind.Destructor:
                node[Properties.IsFinalizer] = true;
                break;
 
            case MethodKind.PropertyGet:
                node[Properties.IsPropertyGet] = true;
                break;
 
            case MethodKind.PropertySet:
                node[Properties.IsPropertySet] = true;
                break;
        }
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForNamespaceAsync(Graph graph, INamespaceSymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForNamespaceAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
        node.AddCategory(CodeNodeCategories.Namespace);
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForNamedTypeAsync(Graph graph, INamedTypeSymbol namedType, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForTypeAsync(namedType, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
        string iconGroupName;
 
        switch (namedType.TypeKind)
        {
            case TypeKind.Class:
                node.AddCategory(CodeNodeCategories.Class);
                iconGroupName = "Class";
                break;
 
            case TypeKind.Delegate:
                node.AddCategory(CodeNodeCategories.Delegate);
                iconGroupName = "Delegate";
                break;
 
            case TypeKind.Enum:
                node.AddCategory(CodeNodeCategories.Enum);
                iconGroupName = "Enum";
                break;
 
            case TypeKind.Interface:
                node.AddCategory(CodeNodeCategories.Interface);
                iconGroupName = "Interface";
                break;
 
            case TypeKind.Module:
                node.AddCategory(CodeNodeCategories.Module);
                iconGroupName = "Module";
                break;
 
            case TypeKind.Struct:
                node.AddCategory(CodeNodeCategories.Struct);
                iconGroupName = "Struct";
                break;
 
            case TypeKind.Error:
                node.AddCategory(CodeNodeCategories.Type);
                iconGroupName = "Error";
                break;
 
            default:
                throw ExceptionUtilities.UnexpectedValue(namedType.TypeKind);
        }
 
        node[DgmlNodeProperties.Icon] = IconHelper.GetIconName(iconGroupName, namedType.DeclaredAccessibility);
        node[RoslynGraphProperties.TypeKind] = namedType.TypeKind;
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForMethodAsync(Graph graph, IMethodSymbol method, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForMemberAsync(method, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
 
        node.AddCategory(CodeNodeCategories.Method);
 
        var isOperator = method.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion;
        node[DgmlNodeProperties.Icon] = isOperator
            ? IconHelper.GetIconName("Operator", method.DeclaredAccessibility)
            : IconHelper.GetIconName("Method", method.DeclaredAccessibility);
 
        node[RoslynGraphProperties.TypeKind] = method.ContainingType.TypeKind;
        node[RoslynGraphProperties.MethodKind] = method.MethodKind;
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForFieldAsync(Graph graph, IFieldSymbol field, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForMemberAsync(field, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
 
        node.AddCategory(CodeNodeCategories.Field);
 
        if (field.ContainingType.TypeKind == TypeKind.Enum)
        {
            node[DgmlNodeProperties.Icon] = IconHelper.GetIconName("EnumMember", field.DeclaredAccessibility);
        }
        else
        {
            node[DgmlNodeProperties.Icon] = IconHelper.GetIconName("Field", field.DeclaredAccessibility);
        }
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForPropertyAsync(Graph graph, IPropertySymbol property, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForMemberAsync(property, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
 
        node.AddCategory(CodeNodeCategories.Property);
 
        node[DgmlNodeProperties.Icon] = IconHelper.GetIconName("Property", property.DeclaredAccessibility);
        node[RoslynGraphProperties.TypeKind] = property.ContainingType.TypeKind;
 
        return node;
    }
 
    private static async Task<GraphNode> GetOrCreateNodeForEventAsync(Graph graph, IEventSymbol eventSymbol, Solution solution, CancellationToken cancellationToken)
    {
        var id = await GraphNodeIdCreation.GetIdForMemberAsync(eventSymbol, solution, cancellationToken).ConfigureAwait(false);
        var node = graph.Nodes.GetOrCreate(id);
 
        node.AddCategory(CodeNodeCategories.Event);
 
        node[DgmlNodeProperties.Icon] = IconHelper.GetIconName("Event", eventSymbol.DeclaredAccessibility);
        node[RoslynGraphProperties.TypeKind] = eventSymbol.ContainingType.TypeKind;
 
        return node;
    }
 
    public void AddLink(GraphNode from, GraphCategory category, GraphNode to, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            Graph.Links.GetOrCreate(from, to).AddCategory(category);
        }
    }
 
    public GraphNode? TryAddNodeForDocument(Document document, CancellationToken cancellationToken)
    {
        // Under the covers, progression will attempt to convert a label into a URI.  Ensure that we
        // can do this safely. before proceeding.
        //
        // The corresponding code on the progression side does: new Uri(text, UriKind.RelativeOrAbsolute)
        // so we check that same kind here.
        var fileName = Path.GetFileName(document.FilePath);
        if (!Uri.TryCreate(fileName, UriKind.RelativeOrAbsolute, out _))
            return null;
 
        using (_gate.DisposableWait(cancellationToken))
        {
            var id = GraphNodeIdCreation.GetIdForDocument(document);
 
            var node = Graph.Nodes.GetOrCreate(id, fileName, CodeNodeCategories.ProjectItem);
 
            _nodeToContextDocumentMap[node] = document;
            _nodeToContextProjectMap[node] = document.Project;
 
            _createdNodes.Add(node);
 
            return node;
        }
    }
 
    public async Task<GraphNode?> CreateNodeAsync(Solution solution, INavigateToSearchResult result, CancellationToken cancellationToken)
    {
        var document = await result.NavigableItem.Document.GetRequiredDocumentAsync(solution, cancellationToken).ConfigureAwait(false);
        var project = document.Project;
 
        // If it doesn't belong to a document or project we can navigate to, then ignore entirely.
        if (document.FilePath == null || project.FilePath == null)
            return null;
 
        var category = result.Kind switch
        {
            NavigateToItemKind.Class => CodeNodeCategories.Class,
            NavigateToItemKind.Delegate => CodeNodeCategories.Delegate,
            NavigateToItemKind.Enum => CodeNodeCategories.Enum,
            NavigateToItemKind.Interface => CodeNodeCategories.Interface,
            NavigateToItemKind.Module => CodeNodeCategories.Module,
            NavigateToItemKind.Structure => CodeNodeCategories.Struct,
            NavigateToItemKind.Method => CodeNodeCategories.Method,
            NavigateToItemKind.Property => CodeNodeCategories.Property,
            NavigateToItemKind.Event => CodeNodeCategories.Event,
            NavigateToItemKind.Constant or
            NavigateToItemKind.EnumItem or
            NavigateToItemKind.Field => CodeNodeCategories.Field,
            _ => null,
        };
 
        // If it's not a category that progression understands, then ignore.
        if (category == null)
            return null;
 
        // Get or make a node for this symbol's containing document that will act as the parent node in the UI.
        var documentNode = this.TryAddNodeForDocument(document, cancellationToken);
        if (documentNode == null)
            return null;
 
        // For purposes of keying this node, just use the display text we will show.  In practice, outside of error
        // scenarios this will be unique and suitable as an ID (esp. as these names are joined with their parent
        // document name to form the full ID).
        var label = result.NavigableItem.DisplayTaggedParts.JoinText();
        var id = documentNode.Id.Add(GraphNodeId.GetLiteral(label));
 
        // If we already have a node that matches this (say there are multiple identical sibling symbols in an error
        // situation).  We just ignore the second match.
        var existing = Graph.Nodes.Get(id);
        if (existing != null)
            return null;
 
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var span = text.Lines.GetLinePositionSpan(NavigateToUtilities.GetBoundedSpan(result.NavigableItem, text));
        var sourceLocation = TryCreateSourceLocation(document.FilePath, span);
        if (sourceLocation == null)
            return null;
 
        var symbolNode = Graph.Nodes.GetOrCreate(id);
 
        symbolNode.Label = label;
        symbolNode.AddCategory(category);
        symbolNode[DgmlNodeProperties.Icon] = GetIconString(result.NavigableItem.Glyph);
        symbolNode[RoslynGraphProperties.ContextDocumentId] = document.Id;
        symbolNode[RoslynGraphProperties.ContextProjectId] = document.Project.Id;
 
        symbolNode[CodeNodeProperties.SourceLocation] = sourceLocation.Value;
 
        this.AddLink(documentNode, GraphCommonSchema.Contains, symbolNode, cancellationToken);
 
        return symbolNode;
    }
 
    public static SourceLocation? TryCreateSourceLocation(string path, LinePositionSpan span)
    {
        // SourceLocation's constructor attempts to create an absolute uri.  So if we can't do that
        // bail out immediately.
        if (!Uri.TryCreate(path, UriKind.Absolute, out var uri))
            return null;
 
        return new SourceLocation(
            uri,
            new Position(span.Start.Line, span.Start.Character),
            new Position(span.End.Line, span.End.Character));
    }
 
    private static string? GetIconString(Glyph glyph)
    {
        var groupName = glyph switch
        {
            Glyph.ClassPublic or Glyph.ClassProtected or Glyph.ClassPrivate or Glyph.ClassInternal => "Class",
            Glyph.ConstantPublic or Glyph.ConstantProtected or Glyph.ConstantPrivate or Glyph.ConstantInternal => "Field",
            Glyph.DelegatePublic or Glyph.DelegateProtected or Glyph.DelegatePrivate or Glyph.DelegateInternal => "Delegate",
            Glyph.EnumPublic or Glyph.EnumProtected or Glyph.EnumPrivate or Glyph.EnumInternal => "Enum",
            Glyph.EnumMemberPublic or Glyph.EnumMemberProtected or Glyph.EnumMemberPrivate or Glyph.EnumMemberInternal => "EnumMember",
            Glyph.ExtensionMethodPublic or Glyph.ExtensionMethodProtected or Glyph.ExtensionMethodPrivate or Glyph.ExtensionMethodInternal => "Method",
            Glyph.EventPublic or Glyph.EventProtected or Glyph.EventPrivate or Glyph.EventInternal => "Event",
            Glyph.FieldPublic or Glyph.FieldProtected or Glyph.FieldPrivate or Glyph.FieldInternal => "Field",
            Glyph.InterfacePublic or Glyph.InterfaceProtected or Glyph.InterfacePrivate or Glyph.InterfaceInternal => "Interface",
            Glyph.MethodPublic or Glyph.MethodProtected or Glyph.MethodPrivate or Glyph.MethodInternal => "Method",
            Glyph.ModulePublic or Glyph.ModuleProtected or Glyph.ModulePrivate or Glyph.ModuleInternal => "Module",
            Glyph.PropertyPublic or Glyph.PropertyProtected or Glyph.PropertyPrivate or Glyph.PropertyInternal => "Property",
            Glyph.StructurePublic or Glyph.StructureProtected or Glyph.StructurePrivate or Glyph.StructureInternal => "Structure",
            _ => null,
        };
 
        if (groupName == null)
            return null;
 
        return IconHelper.GetIconName(groupName, GlyphExtensions.GetAccessibility(GlyphTags.GetTags(glyph)));
    }
 
    public void ApplyToGraph(Graph graph, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            using var graphTransaction = new GraphTransactionScope();
            graph.Merge(this.Graph);
 
            foreach (var deferredProperty in _deferredPropertySets)
            {
                var nodeToSet = graph.Nodes.Get(deferredProperty.Item1.Id);
                nodeToSet.SetValue(deferredProperty.Item2, deferredProperty.Item3);
            }
 
            graphTransaction.Complete();
        }
    }
 
    public void AddDeferredPropertySet(GraphNode node, GraphProperty property, object value, CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            _deferredPropertySets.Add(Tuple.Create(node, property, value));
        }
    }
 
    public Graph Graph { get; } = new();
 
    public ImmutableArray<GraphNode> GetCreatedNodes(CancellationToken cancellationToken)
    {
        using (_gate.DisposableWait(cancellationToken))
        {
            return _createdNodes.ToImmutableArray();
        }
    }
}