Skip to content

Commit

Permalink
feat: add c4 visualizer
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll committed Jul 31, 2024
1 parent c6f4fa3 commit 251c7bb
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 91 deletions.
9 changes: 4 additions & 5 deletions src/Dependify.Cli/Commands/ScanCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace Dependify.Cli.Commands;
using Dependify.Cli.Formatters;
using Dependify.Core;
using Dependify.Core.Graph;
using Depends.Core.Graph;
using Microsoft.Extensions.Logging;

internal class ScanCommand(
Expand Down Expand Up @@ -129,17 +128,17 @@ private void PrintResult(DependencyGraph graph, ScanCommandSettings settings, st

var type = node.Type switch
{
"Project" => "[aquamarine3]Project[/]",
"Solution" => "[red3]Solution[/]",
"Package" => "[skyblue1]Package[/]",
NodeConstants.Project => "[aquamarine3]Project[/]",
NodeConstants.Solution => "[red3]Solution[/]",
NodeConstants.Package => "[skyblue1]Package[/]",
_ => "Unknown"
};

var packagesCountLabel = settings.IncludePackages!.Value
? $"/[royalblue1]{descendants.OfType<PackageReferenceNode>().Count()}[/]"
: string.Empty;

var descendantsLabel = node.Type is not "Package"
var descendantsLabel = node.Type is not NodeConstants.Package
? $"[darkgreen]{descendants.OfType<ProjectReferenceNode>().Count()}{packagesCountLabel}[/]"
: string.Empty;

Expand Down
1 change: 0 additions & 1 deletion src/Dependify.Cli/Commands/ShowCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ namespace Dependify.Cli.Commands;
using Dependify.Cli.Formatters;
using Dependify.Core;
using Dependify.Core.Graph;
using Depends.Core.Graph;
using Microsoft.Extensions.Logging;

internal class ShowCommand(
Expand Down
49 changes: 10 additions & 39 deletions src/Dependify.Cli/Formatters/JsonOutputFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,54 +1,25 @@
namespace Dependify.Cli.Formatters;

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Dependify.Core.Graph;
using Depends.Core.Graph;
using Dependify.Core.Serializers;

internal class JsonOutputFormatter(TextWriter textWriter) : IOutputFormatter
{
private static readonly JsonSerializerOptions JsonOptions =
new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = new PolymorphicTypeResolver()
};

public void Dispose() => textWriter.Dispose();

public void Write<T>(T data)
{
textWriter.WriteLine(JsonSerializer.Serialize(data, JsonOptions));

textWriter.Flush();
}
var graph = data as DependencyGraph;

internal class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
if (graph is null)
{
var jsonTypeInfo = base.GetTypeInfo(type, options);

var baseType = typeof(Node);
if (jsonTypeInfo.Type == baseType)
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "$type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes =
{
new JsonDerivedType(typeof(SolutionReferenceNode), nameof(SolutionReferenceNode)),
new JsonDerivedType(typeof(ProjectReferenceNode), nameof(ProjectReferenceNode)),
new JsonDerivedType(typeof(PackageReferenceNode), nameof(PackageReferenceNode)),
}
};
}

return jsonTypeInfo;
textWriter.WriteLine(JsonGraphSerializer.Serialize(data));
}
else
{
textWriter.WriteLine(JsonGraphSerializer.ToString(data as DependencyGraph));
}

textWriter.Flush();
}
}
1 change: 0 additions & 1 deletion src/Dependify.Core/FileProviderProjectLocator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Dependify.Core;

using Dependify.Core.Graph;
using Depends.Core.Graph;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;

Expand Down
6 changes: 4 additions & 2 deletions src/Dependify.Core/Graph/DependencyGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ public IEnumerable<Node> FindAscendants(Node node)
return this.Edges.Where(edge => edge.End == node).Select(edge => edge.Start).Distinct();
}

public DependencyGraph SubGraph(Node node, Func<Node, bool>? filter = default)
public DependencyGraph SubGraph(Node node, Predicate<Node>? filter = default)
{
var nodes = this.FindAllDescendants(node, filter).Concat([node]).ToList();

// TODO: bug - fix, filter?.Invoke(edge.End) == true

var edges = this.Edges.Where(edge => nodes.Contains(edge.Start) && filter?.Invoke(edge.End) == true).ToList();

return new DependencyGraph(node, nodes, edges);
}

private IEnumerable<Node> FindAllDescendants(Node node, Func<Node, bool>? filter = default)
private IEnumerable<Node> FindAllDescendants(Node node, Predicate<Node>? filter = default)

Check warning on line 41 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 41 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 41 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 41 in src/Dependify.Core/Graph/DependencyGraph.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Change return type of method 'FindAllDescendants' from 'System.Collections.Generic.IEnumerable<Dependify.Core.Graph.Node>' to 'System.Collections.Generic.List<Dependify.Core.Graph.Node>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var nodes = new List<Node>();

Expand Down
10 changes: 9 additions & 1 deletion src/Dependify.Core/Graph/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System.Text.Json.Serialization;


public abstract record Node
{
public string Id { get; protected set; } = default!;
Expand All @@ -14,3 +13,12 @@ public abstract record Node

public abstract string Type { get; }
}

public static class NodeConstants
{
public const string Project = "Project";

public const string Solution = "Solution";

public const string Package = "Package";
}
7 changes: 2 additions & 5 deletions src/Dependify.Core/Graph/PackageReferenceNode.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
namespace Depends.Core.Graph;

using Dependify.Core.Graph;

namespace Dependify.Core.Graph;
public sealed record PackageReferenceNode : Node
{
public PackageReferenceNode(string name, string? version = default)
Expand All @@ -15,6 +12,6 @@ public PackageReferenceNode(string name, string? version = default)
this.Path = $"https://www.nuget.org/packages/{name}/{version}";
}

public override string Type { get; } = "Package";
public override string Type { get; } = NodeConstants.Package;
public string? Version { get; }
}
5 changes: 2 additions & 3 deletions src/Dependify.Core/Graph/ProjectReferenceNode.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Depends.Core.Graph;
namespace Dependify.Core.Graph;

using Dependify.Core;
using Dependify.Core.Graph;

public sealed record ProjectReferenceNode : Node
{
Expand All @@ -17,5 +16,5 @@ public ProjectReferenceNode(string path)
this.Id = file.Name;
}

public override string Type { get; } = "Project";
public override string Type { get; } = NodeConstants.Project;
}
9 changes: 5 additions & 4 deletions src/Dependify.Core/Graph/SolutionReferenceNode.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
namespace Depends.Core.Graph;
namespace Dependify.Core.Graph;

using Dependify.Core;
using Dependify.Core.Graph;

public sealed record SolutionReferenceNode : Node
{
public bool IsEmpty => this.Id == "<default>";

public SolutionReferenceNode(string? path = default)
{
if (!string.IsNullOrWhiteSpace(path))
Expand All @@ -16,9 +17,9 @@ public SolutionReferenceNode(string? path = default)
}
else
{
this.Id = "<empty>";
this.Id = "<default>";
}
}

public override string Type { get; } = "Solution";
public override string Type { get; } = NodeConstants.Solution;
}
1 change: 0 additions & 1 deletion src/Dependify.Core/MsBuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace Dependify.Core;

using Buildalyzer;
using Dependify.Core.Graph;
using Depends.Core.Graph;
using Microsoft.Build.Construction;
using Microsoft.Extensions.Logging;

Expand Down
1 change: 0 additions & 1 deletion src/Dependify.Core/ProjectLocator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Dependify.Core;

using Dependify.Core.Graph;
using Depends.Core.Graph;
using Microsoft.Extensions.Logging;

public class ProjectLocator(ILogger<ProjectLocator> logger)
Expand Down
1 change: 0 additions & 1 deletion src/Dependify.Core/Serializers/GraphVizSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace Dependify.Core.Serializers;

using System.CodeDom.Compiler;
using Dependify.Core.Graph;
using Depends.Core.Graph;

public static class GraphvizSerializer
{
Expand Down
53 changes: 53 additions & 0 deletions src/Dependify.Core/Serializers/JsonGraphSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Dependify.Core.Serializers;

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Dependify.Core.Graph;

public static class JsonGraphSerializer
{
private static readonly JsonSerializerOptions JsonOptions =
new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = new PolymorphicTypeResolver()
};

public static string Serialize<T>(T value) => JsonSerializer.Serialize(value, JsonOptions);

public static string ToString(DependencyGraph graph)
{
ArgumentNullException.ThrowIfNull(graph);

return JsonSerializer.Serialize(graph, JsonOptions);
}

internal class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var jsonTypeInfo = base.GetTypeInfo(type, options);

var baseType = typeof(Node);
if (jsonTypeInfo.Type == baseType)
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "$type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes =
{
new JsonDerivedType(typeof(SolutionReferenceNode), nameof(SolutionReferenceNode)),
new JsonDerivedType(typeof(ProjectReferenceNode), nameof(ProjectReferenceNode)),
new JsonDerivedType(typeof(PackageReferenceNode), nameof(PackageReferenceNode)),
}
};
}

return jsonTypeInfo;
}
}
}
60 changes: 60 additions & 0 deletions src/Dependify.Core/Serializers/MermaidC4Serializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace Dependify.Core.Serializers;

using System.CodeDom.Compiler;
using Dependify.Core.Graph;

public static class MermaidC4Serializer
{
public static string ToString(DependencyGraph graph)
{
ArgumentNullException.ThrowIfNull(graph);

using var stringWriter = new StringWriter();
using var writer = new IndentedTextWriter(stringWriter);

writer.WriteLine("C4Component");
writer.WriteLine($"title {graph.Root.Id}");

Check warning on line 16 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Dereference of a possibly null reference.

Check warning on line 16 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Dereference of a possibly null reference.

Check warning on line 16 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Dereference of a possibly null reference.

Check warning on line 16 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Dereference of a possibly null reference.

var projects = graph.Nodes.Where(n => n.Type == NodeConstants.Project);

foreach (var project in projects)

Check warning on line 20 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 20 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 20 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 20 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)
{
writer.WriteLine($"Container_Boundary({project.Id}, \"{project.Id}\", \"\", \"\") {{");
writer.Indent++;

writer.WriteLine($"Component({project.Id}, \"{project.Id}\", \"Project\", \"\")");

var packages = graph.FindDescendants(project).OfType<PackageReferenceNode>();

if (packages.Any())

Check warning on line 29 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 29 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 29 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 29 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)
{
writer.WriteLine($"Container_Boundary(Packages.{project.Id}, \"Packages\", \"\", \"\") {{");
writer.Indent++;

foreach (var component in packages)

Check warning on line 34 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 34 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 34 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 34 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)
{
writer.WriteLine($"Component({component.Id}, \"{component.Id}\", \"Package\", \"\")");
writer.WriteLine(
$"UpdateElementStyle({component.Id}, $fontColor=\"white\", $bgColor=\"grey\", $borderColor=\"#99CB0E\")"
);
}
writer.Indent--;
writer.WriteLine("}");
}

writer.Indent--;
writer.WriteLine("}");
}

foreach (var project in projects)

Check warning on line 49 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 49 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 49 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)

Check warning on line 49 in src/Dependify.Core/Serializers/MermaidC4Serializer.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Possible multiple enumerations of 'IEnumerable' collection. Consider using an implementation that avoids multiple enumerations. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1851)
{
foreach (var child in graph.FindDescendants(project).OfType<ProjectReferenceNode>())
{
writer.WriteLine($"Rel({project.Id}, {child.Id}, \"\")");
}
}

writer.Flush();
return stringWriter.ToString();
}
}
1 change: 0 additions & 1 deletion src/Dependify.Core/Serializers/MermaidSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace Dependify.Core.Serializers;

using System.CodeDom.Compiler;
using Dependify.Core.Graph;
using Depends.Core.Graph;

public static class MermaidSerializer
{
Expand Down
9 changes: 7 additions & 2 deletions src/Dependify.Core/SolutionRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Dependify.Core;

using Dependify.Core.Graph;
using Depends.Core.Graph;

public class SolutionRegistry(FileProviderProjectLocator projectLocator, MsBuildService buildService)

Check warning on line 5 in src/Dependify.Core/SolutionRegistry.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Non-nullable property 'Nodes' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in src/Dependify.Core/SolutionRegistry.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Non-nullable property 'Nodes' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in src/Dependify.Core/SolutionRegistry.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Non-nullable property 'Nodes' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 5 in src/Dependify.Core/SolutionRegistry.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

Non-nullable property 'Nodes' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{
Expand All @@ -23,6 +22,7 @@ public void LoadRegistry()

this.Solutions.Add(solution);
}

this.Nodes = nodes;
}

Expand All @@ -38,7 +38,12 @@ public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken ca

var solution = this.Solutions[i];

var dependencyGraph = this.buildService.AnalyzeReferences(solution, msBuildConfig);
var dependencyGraph = solution.IsEmpty
? this.buildService.AnalyzeReferences(
this.Nodes.OfType<ProjectReferenceNode>().ToList(),
msBuildConfig
)
: this.buildService.AnalyzeReferences(solution, msBuildConfig);

// TODO: add cache lookup for already loaded solutions
this.solutionGraphs.Add(solution, dependencyGraph);
Expand Down
1 change: 0 additions & 1 deletion src/Dependify.Core/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Dependify.Core;

using Dependify.Core.Graph;
using Depends.Core.Graph;

public static class Utils
{
Expand Down
Loading

0 comments on commit 251c7bb

Please sign in to comment.