|
// 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();
}
}
}
|