From 18758cdbbeb811df473bf3fab9300e8b336e1cdf Mon Sep 17 00:00:00 2001 From: magic56 Date: Thu, 19 Dec 2024 16:55:21 +0100 Subject: [PATCH 1/3] feat: enhance DependencyRelation model with namespace and assembly properties; add degree calculation for dependencies --- CodeLineCounter.Tests/DataExporterTests.cs | 2 +- .../DependencyGraphGeneratorTests.cs | 4 +- CodeLineCounter/CodeLineCounter.csproj | 1 + CodeLineCounter/Models/DependencyRelation.cs | 36 ++++- .../Services/DependencyAnalyzer.cs | 42 +++++- .../Services/DependencyGraphGenerator.cs | 123 ++++++++++++++++-- 6 files changed, 188 insertions(+), 20 deletions(-) diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index e39eadf..c5fc571 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -217,7 +217,7 @@ public void export_dependencies_creates_files_in_correct_formats() // Arrange var dependencies = new List { - new DependencyRelation { SourceClass = "ClassA", TargetClass = "ClassB", FilePath = "file1.cs", StartLine = 10 }, + new DependencyRelation { SourceClass = "ClassA", SourceNamespace = "NamespaceA", SourceAssembly = "AssemblyA", TargetClass = "ClassB", TargetNamespace = "NamespaceB", TargetAssembly = "AssemblyB", FilePath = "file1.cs", StartLine = 10 }, }; var testFilePath = "test_export"; diff --git a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs index 1e8a434..221bfb6 100644 --- a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -11,8 +11,8 @@ public void generate_graph_with_valid_dependencies_creates_dot_file() // Arrange var dependencies = new List { - new DependencyRelation { SourceClass = "ClassA", TargetClass = "ClassB" , FilePath = "path/to/file", StartLine = 1}, - new DependencyRelation { SourceClass = "ClassB", TargetClass = "ClassC", FilePath = "path/to/file", StartLine = 1} + new DependencyRelation { SourceClass = "ClassA", SourceNamespace = "NamespaceA", SourceAssembly = "AssemblyA", TargetClass = "ClassB" , TargetNamespace = "NamespaceB", TargetAssembly = "AssemblyB", FilePath = "path/to/file", StartLine = 1}, + new DependencyRelation { SourceClass = "ClassB", SourceNamespace = "NamespaceB", SourceAssembly = "AssemblyB", TargetClass = "ClassC", TargetNamespace = "NamespaceB", TargetAssembly = "AssemblyB", FilePath = "path/to/file", StartLine = 1} }; string outputPath = Path.Combine(Path.GetTempPath(), "test_graph.dot"); diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index c5455e2..32101db 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -15,6 +15,7 @@ + diff --git a/CodeLineCounter/Models/DependencyRelation.cs b/CodeLineCounter/Models/DependencyRelation.cs index 284dafe..6ad2410 100644 --- a/CodeLineCounter/Models/DependencyRelation.cs +++ b/CodeLineCounter/Models/DependencyRelation.cs @@ -1,4 +1,5 @@ using CsvHelper.Configuration.Attributes; + namespace CodeLineCounter.Models { public class DependencyRelation @@ -6,29 +7,60 @@ public class DependencyRelation [Name("SourceClass")] public required string SourceClass { get; set; } + [Name("SourceNamespace")] + public required string SourceNamespace { get; set; } + + [Name("SourceAssembly")] + public required string SourceAssembly { get; set; } + [Name("TargetClass")] public required string TargetClass { get; set; } + [Name("TargetNamespace")] + public required string TargetNamespace { get; set; } + + [Name("TargetAssembly")] + public required string TargetAssembly { get; set; } + [Name("FilePath")] public required string FilePath { get; set; } [Name("StartLine")] public int StartLine { get; set; } + [Name("IncomingDegree")] + public int IncomingDegree { get; set; } + + [Name("OutgoingDegree")] + public int OutgoingDegree { get; set; } + + public override bool Equals(object? obj) { if (obj is not DependencyRelation other) return false; return SourceClass == other.SourceClass && + SourceNamespace == other.SourceNamespace && + SourceAssembly == other.SourceAssembly && TargetClass == other.TargetClass && + TargetNamespace == other.TargetNamespace && + TargetAssembly == other.TargetAssembly && FilePath == other.FilePath && StartLine == other.StartLine; } public override int GetHashCode() { - return HashCode.Combine(SourceClass, TargetClass, FilePath, StartLine); + return HashCode.Combine( + SourceClass, + SourceNamespace, + SourceAssembly, + TargetClass, + TargetNamespace, + TargetAssembly, + FilePath, + StartLine); } } -} +} \ No newline at end of file diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index deddb6c..6179765 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -112,7 +112,11 @@ public static void AnalyzeFile(string filePath, string sourceCode) var relation = new DependencyRelation { SourceClass = GetFullTypeName(classDeclaration), + SourceNamespace = classDeclaration.Ancestors().OfType().FirstOrDefault()?.Name.ToString() ?? "", + SourceAssembly = classDeclaration.SyntaxTree.GetRoot().DescendantNodes().OfType().FirstOrDefault()?.Usings.FirstOrDefault()?.Name.ToString() ?? "", TargetClass = dependency, + TargetNamespace = dependency.Contains(".") ? dependency.Substring(0, dependency.LastIndexOf('.')) : "", + TargetAssembly = dependency.Contains(".") ? dependency.Substring(0, dependency.LastIndexOf('.')) : "", FilePath = filePath, StartLine = classDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line }; @@ -140,7 +144,7 @@ private static IEnumerable ExtractDependencies(ClassDeclarationSyntax cl { var dependencies = new HashSet(); var usings = GetUsingsWithCurrentNamespace(classDeclaration); - + AnalyzeInheritance(classDeclaration, usings, dependencies); AnalyzeClassMembers(classDeclaration, usings, dependencies); @@ -335,7 +339,40 @@ private static List SplitGenericType(string typeName) public static List GetDependencies() { - return _dependencyMap.SelectMany(kvp => kvp.Value).ToList() ?? new List(); + var allDependencies = _dependencyMap.SelectMany(kvp => kvp.Value).ToList(); + CalculateDegrees(allDependencies); + return allDependencies; + } + + private static void CalculateDegrees(List dependencies) + { + var incomingDegrees = new ConcurrentDictionary(); + var outgoingDegrees = new ConcurrentDictionary(); + + // Calculate degrees + foreach (var dep in dependencies) + { + // Outgoing degree + outgoingDegrees.AddOrUpdate( + dep.SourceClass, + 1, + (key, count) => count + 1 + ); + + // Incoming degree + incomingDegrees.AddOrUpdate( + dep.TargetClass, + 1, + (key, count) => count + 1 + ); + } + + // Update relations with calculated degrees + foreach (var dep in dependencies) + { + dep.OutgoingDegree = outgoingDegrees.GetValueOrDefault(dep.SourceClass); + dep.IncomingDegree = incomingDegrees.GetValueOrDefault(dep.TargetClass); + } } public static void Clear() @@ -343,5 +380,6 @@ public static void Clear() _dependencyMap.Clear(); _solutionClasses.Clear(); } + } } \ No newline at end of file diff --git a/CodeLineCounter/Services/DependencyGraphGenerator.cs b/CodeLineCounter/Services/DependencyGraphGenerator.cs index 4748a25..984037a 100644 --- a/CodeLineCounter/Services/DependencyGraphGenerator.cs +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -1,36 +1,133 @@ using CodeLineCounter.Models; using QuikGraph; using QuikGraph.Graphviz; +using QuikGraph.Graphviz.Dot; namespace CodeLineCounter.Services { - public static class DependencyGraphGenerator { - public static void GenerateGraph(List dependencies, string outputPath) + public static void GenerateGraph(List dependencies, string outputPath, string? filterNamespace = null, string? filterAssembly = null) { - var graph = new AdjacencyGraph>(); + var filteredDependencies = dependencies; + filteredDependencies = FilterNamespaceFromDependencies(dependencies, filterNamespace, filteredDependencies); + filteredDependencies = FilterAssemblyFromDependencies(filterAssembly, filteredDependencies); + + var graph = new BidirectionalGraph>(); + var vertexInfo = new Dictionary(); + var namespaceGroups = new Dictionary>(); - foreach (var dependency in dependencies) + // Collect degree information and group by namespace + foreach (var dep in filteredDependencies) { - if (!graph.ContainsVertex(dependency.SourceClass)) - graph.AddVertex(dependency.SourceClass); - if (!graph.ContainsVertex(dependency.TargetClass)) - graph.AddVertex(dependency.TargetClass); + var sourceLabel = GetVertexLabel(dep.SourceClass, dep.SourceNamespace, dep.SourceAssembly); + var targetLabel = GetVertexLabel(dep.TargetClass, dep.TargetNamespace, dep.TargetAssembly); + + if (!vertexInfo.ContainsKey(sourceLabel)) + { + vertexInfo[sourceLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } + if (!vertexInfo.ContainsKey(targetLabel)) + { + vertexInfo[targetLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } + + if (!graph.ContainsVertex(sourceLabel)) + graph.AddVertex(sourceLabel); + if (!graph.ContainsVertex(targetLabel)) + graph.AddVertex(targetLabel); - graph.AddEdge(new Edge(dependency.SourceClass, dependency.TargetClass)); + graph.AddEdge(new Edge(sourceLabel, targetLabel)); + + // Group by namespace + if (!namespaceGroups.ContainsKey(dep.SourceNamespace)) + { + namespaceGroups[dep.SourceNamespace] = new List(); + } + if (!namespaceGroups.ContainsKey(dep.TargetNamespace)) + { + namespaceGroups[dep.TargetNamespace] = new List(); + } + + if (!namespaceGroups[dep.SourceNamespace].Contains(sourceLabel)) + { + namespaceGroups[dep.SourceNamespace].Add(sourceLabel); + } + if (!namespaceGroups[dep.TargetNamespace].Contains(targetLabel)) + { + namespaceGroups[dep.TargetNamespace].Add(targetLabel); + } } + var graphviz = new GraphvizAlgorithm>(graph); graphviz.FormatVertex += (sender, args) => { - args.VertexFormat.Label = args.Vertex; - args.VertexFormat.Shape = QuikGraph.Graphviz.Dot.GraphvizVertexShape.Box; + var info = vertexInfo[args.Vertex]; + args.VertexFormat.Label = $"{args.Vertex}\\nIn: {info.incoming}, Out: {info.outgoing}"; + args.VertexFormat.Shape = GraphvizVertexShape.Box; + + // Color the nodes based on their degrees + if (info.incoming > info.outgoing) + { + args.VertexFormat.Style = GraphvizVertexStyle.Filled; + args.VertexFormat.FillColor = GraphvizColor.LightBlue; + } + else if (info.incoming < info.outgoing) + { + args.VertexFormat.Style = GraphvizVertexStyle.Filled; + args.VertexFormat.FillColor = GraphvizColor.LightCoral; + } + }; + + graphviz.FormatCluster += (sender, args) => + { + args.GraphFormat.Label = args.Cluster.ToString(); }; - string dot = graphviz.Generate(); + // foreach (var nsGroup in namespaceGroups) + // { + // //var cluster = graphviz.Clusters.Add(nsGroup.Key); + // // foreach (var vertex in nsGroup.Value) + // // { + // // graph.AddVertexToCluster(cluster, vertex); + // // cluster.AddVertex(vertex); + // // } + // } + + var dot = graphviz.Generate(); File.WriteAllText(outputPath, dot); - } + + private static List FilterAssemblyFromDependencies(string? filterAssembly, List filteredDependencies) + { + if (!string.IsNullOrEmpty(filterAssembly)) + { + filteredDependencies = filteredDependencies.Where(d => + d.SourceAssembly.Equals(filterAssembly) || + d.TargetAssembly.Equals(filterAssembly)).ToList(); + } + + return filteredDependencies; + } + + private static List FilterNamespaceFromDependencies(List dependencies, string? filterNamespace, List filteredDependencies) + { + if (!string.IsNullOrEmpty(filterNamespace)) + { + filteredDependencies = dependencies.Where(d => + d.SourceNamespace.Contains(filterNamespace) || + d.TargetNamespace.Contains(filterNamespace)).ToList(); + } + + return filteredDependencies; + } + + private static string GetVertexLabel(string className, string namespaceName, string assemblyName) + { + return $"{className}\\n{namespaceName}\\n{assemblyName}"; + } + + } } \ No newline at end of file From aac0a9c4af7e3230fe08de1ba1d6f95c182d438a Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Sat, 21 Dec 2024 17:53:59 +0100 Subject: [PATCH 2/3] feat: update project to use DotNetGraph package and enhance dependency graph generation --- .vscode/launch.json | 2 +- .../DependencyGraphGeneratorTests.cs | 8 +- CodeLineCounter/CodeLineCounter.csproj | 1 + .../Services/DependencyAnalyzer.cs | 11 +- .../Services/DependencyGraphGenerator.cs | 192 +++++++++++------- CodeLineCounter/Utils/DataExporter.cs | 4 +- 6 files changed, 133 insertions(+), 85 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0d50283..b1cf6dd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "C#: CodeLineCounter - new", "type": "coreclr", "request": "launch", - "program": "${workspaceFolder}/CodeLineCounter/bin/Debug/net8.0/CodeLineCounter.dll", + "program": "${workspaceFolder}/CodeLineCounter/bin/Debug/net9.0/CodeLineCounter.dll", "args": ["-d", "${workspaceFolder}"], "cwd": "${workspaceFolder}", "stopAtEntry": false, diff --git a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs index 221bfb6..86d4249 100644 --- a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -6,7 +6,7 @@ namespace CodeLineCounter.Tests public class DependencyGraphGeneratorTests { [Fact] - public void generate_graph_with_valid_dependencies_creates_dot_file() + public async Task generate_graph_with_valid_dependencies_creates_dot_file() { // Arrange var dependencies = new List @@ -18,7 +18,7 @@ public void generate_graph_with_valid_dependencies_creates_dot_file() string outputPath = Path.Combine(Path.GetTempPath(), "test_graph.dot"); // Act - DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + await DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); // Assert Assert.True(File.Exists(outputPath)); @@ -32,14 +32,14 @@ public void generate_graph_with_valid_dependencies_creates_dot_file() // Empty dependencies list [Fact] - public void generate_graph_with_empty_dependencies_creates_empty_graph() + public async Task generate_graph_with_empty_dependencies_creates_empty_graph() { // Arrange var dependencies = new List(); string outputPath = Path.Combine(Path.GetTempPath(), "empty_graph.dot"); // Act - DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + await DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); // Assert Assert.True(File.Exists(outputPath)); diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index 32101db..4afb1b7 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -14,6 +14,7 @@ all + diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index 6179765..b9a6a40 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -74,6 +74,11 @@ private static string GetFullTypeName(ClassDeclarationSyntax classDeclaration) return classDeclaration.Identifier.Text; } + private static string GetSimpleTypeName(ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Identifier.Text; + } + public static void AnalyzeProjects(IEnumerable projectFiles) { foreach (var projectFile in projectFiles) @@ -104,17 +109,17 @@ public static void AnalyzeFile(string filePath, string sourceCode) Parallel.ForEach(classes, classDeclaration => { - var className = GetFullTypeName(classDeclaration); + var className = GetSimpleTypeName(classDeclaration); var dependencies = ExtractDependencies(classDeclaration); foreach (var dependency in dependencies) { var relation = new DependencyRelation { - SourceClass = GetFullTypeName(classDeclaration), + SourceClass = GetSimpleTypeName(classDeclaration), SourceNamespace = classDeclaration.Ancestors().OfType().FirstOrDefault()?.Name.ToString() ?? "", SourceAssembly = classDeclaration.SyntaxTree.GetRoot().DescendantNodes().OfType().FirstOrDefault()?.Usings.FirstOrDefault()?.Name.ToString() ?? "", - TargetClass = dependency, + TargetClass = dependency.Split('.')[(dependency.Split('.').Length - 1)], TargetNamespace = dependency.Contains(".") ? dependency.Substring(0, dependency.LastIndexOf('.')) : "", TargetAssembly = dependency.Contains(".") ? dependency.Substring(0, dependency.LastIndexOf('.')) : "", FilePath = filePath, diff --git a/CodeLineCounter/Services/DependencyGraphGenerator.cs b/CodeLineCounter/Services/DependencyGraphGenerator.cs index 984037a..fe856f0 100644 --- a/CodeLineCounter/Services/DependencyGraphGenerator.cs +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -1,102 +1,150 @@ using CodeLineCounter.Models; -using QuikGraph; -using QuikGraph.Graphviz; -using QuikGraph.Graphviz.Dot; +using DotNetGraph; +using DotNetGraph.Attributes; +using DotNetGraph.Compilation; +using DotNetGraph.Core; +using DotNetGraph.Extensions; namespace CodeLineCounter.Services { public static class DependencyGraphGenerator { - public static void GenerateGraph(List dependencies, string outputPath, string? filterNamespace = null, string? filterAssembly = null) + public static async Task GenerateGraph(List dependencies, string outputPath, string? filterNamespace = null, string? filterAssembly = null) { var filteredDependencies = dependencies; filteredDependencies = FilterNamespaceFromDependencies(dependencies, filterNamespace, filteredDependencies); filteredDependencies = FilterAssemblyFromDependencies(filterAssembly, filteredDependencies); - var graph = new BidirectionalGraph>(); + var graph = new DotGraph(); + graph.Directed = true; + var vertexInfo = new Dictionary(); var namespaceGroups = new Dictionary>(); - + graph.WithLabel("DependencyGraph"); + graph.WithIdentifier("DependencyGraph", true); // Collect degree information and group by namespace foreach (var dep in filteredDependencies) { - var sourceLabel = GetVertexLabel(dep.SourceClass, dep.SourceNamespace, dep.SourceAssembly); - var targetLabel = GetVertexLabel(dep.TargetClass, dep.TargetNamespace, dep.TargetAssembly); + GroupByNamespace(vertexInfo, namespaceGroups, dep); + } - if (!vertexInfo.ContainsKey(sourceLabel)) - { - vertexInfo[sourceLabel] = (dep.IncomingDegree, dep.OutgoingDegree); - } - if (!vertexInfo.ContainsKey(targetLabel)) + // Create clusters and add nodes + foreach (var nsGroup in namespaceGroups) + { + DotSubgraph cluster = CreateCluster(nsGroup); + + foreach (var vertex in nsGroup.Value) { - vertexInfo[targetLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + DotNode node = CreateNode(vertexInfo, vertex); + + cluster.Elements.Add(node); } - if (!graph.ContainsVertex(sourceLabel)) - graph.AddVertex(sourceLabel); - if (!graph.ContainsVertex(targetLabel)) - graph.AddVertex(targetLabel); + graph.Elements.Add(cluster); + } - graph.AddEdge(new Edge(sourceLabel, targetLabel)); + // Add edges + foreach (var dep in filteredDependencies) + { + DotEdge edge = CreateEdge(dep); + graph.Elements.Add(edge); + } - // Group by namespace - if (!namespaceGroups.ContainsKey(dep.SourceNamespace)) - { - namespaceGroups[dep.SourceNamespace] = new List(); - } - if (!namespaceGroups.ContainsKey(dep.TargetNamespace)) - { - namespaceGroups[dep.TargetNamespace] = new List(); - } + await CompileGraphAndWriteToFile(outputPath, graph); + } - if (!namespaceGroups[dep.SourceNamespace].Contains(sourceLabel)) - { - namespaceGroups[dep.SourceNamespace].Add(sourceLabel); - } - if (!namespaceGroups[dep.TargetNamespace].Contains(targetLabel)) - { - namespaceGroups[dep.TargetNamespace].Add(targetLabel); - } + private static DotEdge CreateEdge(DependencyRelation dep) + { + var sourceLabel = dep.SourceClass; + var targetLabel = dep.TargetClass; + + var edge = new DotEdge(); + var dotidentiferFrom = new DotIdentifier(sourceLabel); + var dotidentiferTo = new DotIdentifier(targetLabel); + + edge.From = dotidentiferFrom; + edge.To = dotidentiferTo; + return edge; + } + + private static DotSubgraph CreateCluster(KeyValuePair> nsGroup) + { + var cluster = new DotSubgraph(); + cluster.WithLabel($"cluster_{nsGroup.Key.Replace(".", "_")}"); + cluster.WithIdentifier($"cluster_{nsGroup.Key.Replace(".", "_")}", true); + cluster.Label = nsGroup.Key; + return cluster; + } + + private static DotNode CreateNode(Dictionary vertexInfo, string vertex) + { + var info = vertexInfo[vertex]; + var node = new DotNode(); + node.WithIdentifier(vertex, true); + node.Label = $"{vertex}" +Environment.NewLine + $"\nIn: {info.incoming}, Out: {info.outgoing}"; + node.Shape = DotNodeShape.Box; + + // Color nodes based on degrees + if (info.incoming > info.outgoing) + { + node.FillColor = "lightblue"; + node.Style = DotNodeStyle.Filled; } + else if (info.incoming < info.outgoing) + { + node.FillColor = "lightcoral"; + node.Style = DotNodeStyle.Filled; + } + + return node; + } + private static void GroupByNamespace(Dictionary vertexInfo, Dictionary> namespaceGroups, DependencyRelation dep) + { + var sourceLabel = dep.SourceClass; + var targetLabel = dep.TargetClass; - var graphviz = new GraphvizAlgorithm>(graph); - graphviz.FormatVertex += (sender, args) => + if (!vertexInfo.ContainsKey(sourceLabel)) + { + vertexInfo[sourceLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } + if (!vertexInfo.ContainsKey(targetLabel)) { - var info = vertexInfo[args.Vertex]; - args.VertexFormat.Label = $"{args.Vertex}\\nIn: {info.incoming}, Out: {info.outgoing}"; - args.VertexFormat.Shape = GraphvizVertexShape.Box; + vertexInfo[targetLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } - // Color the nodes based on their degrees - if (info.incoming > info.outgoing) - { - args.VertexFormat.Style = GraphvizVertexStyle.Filled; - args.VertexFormat.FillColor = GraphvizColor.LightBlue; - } - else if (info.incoming < info.outgoing) - { - args.VertexFormat.Style = GraphvizVertexStyle.Filled; - args.VertexFormat.FillColor = GraphvizColor.LightCoral; - } - }; + // Group by namespace + if (!namespaceGroups.ContainsKey(dep.SourceNamespace)) + { + namespaceGroups[dep.SourceNamespace] = new List(); + } + if (!namespaceGroups.ContainsKey(dep.TargetNamespace)) + { + namespaceGroups[dep.TargetNamespace] = new List(); + } - graphviz.FormatCluster += (sender, args) => + if (!namespaceGroups[dep.SourceNamespace].Contains(dep.SourceClass)) + { + namespaceGroups[dep.SourceNamespace].Add(dep.SourceClass); + } + if (!namespaceGroups[dep.TargetNamespace].Contains(dep.TargetClass)) { - args.GraphFormat.Label = args.Cluster.ToString(); - }; - - // foreach (var nsGroup in namespaceGroups) - // { - // //var cluster = graphviz.Clusters.Add(nsGroup.Key); - // // foreach (var vertex in nsGroup.Value) - // // { - // // graph.AddVertexToCluster(cluster, vertex); - // // cluster.AddVertex(vertex); - // // } - // } - - var dot = graphviz.Generate(); - File.WriteAllText(outputPath, dot); + namespaceGroups[dep.TargetNamespace].Add(dep.TargetClass); + } + } + + private static async Task CompileGraphAndWriteToFile(string outputPath, DotGraph graph) + { + await using var writer = new StringWriter(); + var options = new CompilationOptions(); + options.Indented = true; + var context = new CompilationContext(writer, options); + graph.Directed = true; + context.DirectedGraph = true; + + await graph.CompileAsync(context); + var result = writer.GetStringBuilder().ToString(); + await File.WriteAllTextAsync(outputPath, result); } private static List FilterAssemblyFromDependencies(string? filterAssembly, List filteredDependencies) @@ -123,11 +171,5 @@ private static List FilterNamespaceFromDependencies(List dup ExportCollection(filePath, duplications, format); } - public static void ExportDependencies(string filePath, List dependencies,CoreUtils.ExportFormat format) + public static async void ExportDependencies(string filePath, List dependencies,CoreUtils.ExportFormat format) { string outputFilePath = CoreUtils.GetExportFileNameWithExtension(filePath, format); ExportCollection(outputFilePath, dependencies, format); - DependencyGraphGenerator.GenerateGraph(dependencies, Path.ChangeExtension(outputFilePath, ".dot")); + await DependencyGraphGenerator.GenerateGraph(dependencies, Path.ChangeExtension(outputFilePath, ".dot")); } public static void ExportMetrics(string filePath, List metrics, From 4812c7a9e655ac276befa32704ed50dfab3509cb Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Sat, 21 Dec 2024 18:27:36 +0100 Subject: [PATCH 3/3] feat: update DependencyGraphGenerator and DataExporter for async file operations and improve color handling --- .../DependencyGraphGeneratorTests.cs | 4 ++-- .../Services/DependencyGraphGenerator.cs | 13 +++++++------ CodeLineCounter/Services/SolutionAnalyzer.cs | 2 +- CodeLineCounter/Utils/DataExporter.cs | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs index 86d4249..b6b8fc2 100644 --- a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -22,7 +22,7 @@ public async Task generate_graph_with_valid_dependencies_creates_dot_file() // Assert Assert.True(File.Exists(outputPath)); - string content = File.ReadAllText(outputPath); + string content = await File.ReadAllTextAsync(outputPath); Assert.Contains("ClassA", content); Assert.Contains("ClassB", content); Assert.Contains("ClassC", content); @@ -43,7 +43,7 @@ public async Task generate_graph_with_empty_dependencies_creates_empty_graph() // Assert Assert.True(File.Exists(outputPath)); - string content = File.ReadAllText(outputPath); + string content = await File.ReadAllTextAsync(outputPath); Assert.Contains("digraph", content); Assert.DoesNotContain("->", content); diff --git a/CodeLineCounter/Services/DependencyGraphGenerator.cs b/CodeLineCounter/Services/DependencyGraphGenerator.cs index fe856f0..0dfc6fb 100644 --- a/CodeLineCounter/Services/DependencyGraphGenerator.cs +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -59,11 +59,11 @@ private static DotEdge CreateEdge(DependencyRelation dep) var targetLabel = dep.TargetClass; var edge = new DotEdge(); - var dotidentiferFrom = new DotIdentifier(sourceLabel); - var dotidentiferTo = new DotIdentifier(targetLabel); + var dotIdentifierFrom = new DotIdentifier(sourceLabel); + var dotIdentifierTo = new DotIdentifier(targetLabel); - edge.From = dotidentiferFrom; - edge.To = dotidentiferTo; + edge.From = dotIdentifierFrom; + edge.To = dotIdentifierTo; return edge; } @@ -73,6 +73,7 @@ private static DotSubgraph CreateCluster(KeyValuePair> nsGr cluster.WithLabel($"cluster_{nsGroup.Key.Replace(".", "_")}"); cluster.WithIdentifier($"cluster_{nsGroup.Key.Replace(".", "_")}", true); cluster.Label = nsGroup.Key; + cluster.Style = DotSubgraphStyle.Filled; return cluster; } @@ -87,12 +88,12 @@ private static DotNode CreateNode(Dictionary info.outgoing) { - node.FillColor = "lightblue"; + node.FillColor = DotColor.LightGreen; node.Style = DotNodeStyle.Filled; } else if (info.incoming < info.outgoing) { - node.FillColor = "lightcoral"; + node.FillColor = DotColor.LightSalmon; node.Style = DotNodeStyle.Filled; } diff --git a/CodeLineCounter/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs index df3c579..65f71db 100644 --- a/CodeLineCounter/Services/SolutionAnalyzer.cs +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -89,7 +89,7 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor duplicationOutputFilePath, result.DuplicationMap, format), - () => DataExporter.ExportDependencies( + async () => await DataExporter.ExportDependencies( dependenciesOutputFilePath, result.DependencyList, format) diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index d1b14ac..2c78d52 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -44,7 +44,7 @@ public static void ExportDuplications(string filePath, List dup ExportCollection(filePath, duplications, format); } - public static async void ExportDependencies(string filePath, List dependencies,CoreUtils.ExportFormat format) + public static async Task ExportDependencies(string filePath, List dependencies,CoreUtils.ExportFormat format) { string outputFilePath = CoreUtils.GetExportFileNameWithExtension(filePath, format); ExportCollection(outputFilePath, dependencies, format);