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/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..b6b8fc2 100644 --- a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -6,23 +6,23 @@ 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 { - 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"); // Act - DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + await DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); // 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); @@ -32,18 +32,18 @@ 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)); - string content = File.ReadAllText(outputPath); + string content = await File.ReadAllTextAsync(outputPath); Assert.Contains("digraph", content); Assert.DoesNotContain("->", content); diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index c5455e2..4afb1b7 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -14,7 +14,9 @@ all + + 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..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,15 +109,19 @@ 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), - TargetClass = dependency, + SourceClass = GetSimpleTypeName(classDeclaration), + SourceNamespace = classDeclaration.Ancestors().OfType().FirstOrDefault()?.Name.ToString() ?? "", + SourceAssembly = classDeclaration.SyntaxTree.GetRoot().DescendantNodes().OfType().FirstOrDefault()?.Usings.FirstOrDefault()?.Name.ToString() ?? "", + 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, StartLine = classDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line }; @@ -140,7 +149,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 +344,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 +385,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..0dfc6fb 100644 --- a/CodeLineCounter/Services/DependencyGraphGenerator.cs +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -1,36 +1,176 @@ using CodeLineCounter.Models; -using QuikGraph; -using QuikGraph.Graphviz; +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) + 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 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) + { + GroupByNamespace(vertexInfo, namespaceGroups, dep); + } + + // Create clusters and add nodes + foreach (var nsGroup in namespaceGroups) + { + DotSubgraph cluster = CreateCluster(nsGroup); + + foreach (var vertex in nsGroup.Value) + { + DotNode node = CreateNode(vertexInfo, vertex); + + cluster.Elements.Add(node); + } + + graph.Elements.Add(cluster); + } + + // Add edges + foreach (var dep in filteredDependencies) + { + DotEdge edge = CreateEdge(dep); + graph.Elements.Add(edge); + } + + await CompileGraphAndWriteToFile(outputPath, graph); + } + + private static DotEdge CreateEdge(DependencyRelation dep) + { + var sourceLabel = dep.SourceClass; + var targetLabel = dep.TargetClass; + + var edge = new DotEdge(); + var dotIdentifierFrom = new DotIdentifier(sourceLabel); + var dotIdentifierTo = new DotIdentifier(targetLabel); + + edge.From = dotIdentifierFrom; + edge.To = dotIdentifierTo; + 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; + cluster.Style = DotSubgraphStyle.Filled; + 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 = DotColor.LightGreen; + node.Style = DotNodeStyle.Filled; + } + else if (info.incoming < info.outgoing) + { + node.FillColor = DotColor.LightSalmon; + node.Style = DotNodeStyle.Filled; + } + + return node; + } + + private static void GroupByNamespace(Dictionary vertexInfo, Dictionary> namespaceGroups, DependencyRelation dep) { - var graph = new AdjacencyGraph>(); + var sourceLabel = dep.SourceClass; + var targetLabel = dep.TargetClass; + + if (!vertexInfo.ContainsKey(sourceLabel)) + { + vertexInfo[sourceLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } + if (!vertexInfo.ContainsKey(targetLabel)) + { + vertexInfo[targetLabel] = (dep.IncomingDegree, dep.OutgoingDegree); + } + + // Group by namespace + if (!namespaceGroups.ContainsKey(dep.SourceNamespace)) + { + namespaceGroups[dep.SourceNamespace] = new List(); + } + if (!namespaceGroups.ContainsKey(dep.TargetNamespace)) + { + namespaceGroups[dep.TargetNamespace] = new List(); + } - foreach (var dependency in dependencies) + if (!namespaceGroups[dep.SourceNamespace].Contains(dep.SourceClass)) { - if (!graph.ContainsVertex(dependency.SourceClass)) - graph.AddVertex(dependency.SourceClass); - if (!graph.ContainsVertex(dependency.TargetClass)) - graph.AddVertex(dependency.TargetClass); + namespaceGroups[dep.SourceNamespace].Add(dep.SourceClass); + } + if (!namespaceGroups[dep.TargetNamespace].Contains(dep.TargetClass)) + { + 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); + } - graph.AddEdge(new Edge(dependency.SourceClass, dependency.TargetClass)); + 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(); } - var graphviz = new GraphvizAlgorithm>(graph); - graphviz.FormatVertex += (sender, args) => + return filteredDependencies; + } + + private static List FilterNamespaceFromDependencies(List dependencies, string? filterNamespace, List filteredDependencies) + { + if (!string.IsNullOrEmpty(filterNamespace)) { - args.VertexFormat.Label = args.Vertex; - args.VertexFormat.Shape = QuikGraph.Graphviz.Dot.GraphvizVertexShape.Box; - }; + filteredDependencies = dependencies.Where(d => + d.SourceNamespace.Contains(filterNamespace) || + d.TargetNamespace.Contains(filterNamespace)).ToList(); + } - string dot = graphviz.Generate(); - File.WriteAllText(outputPath, dot); - + return filteredDependencies; } + } } \ No newline at end of file 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 412af07..2c78d52 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -44,12 +44,12 @@ public static void ExportDuplications(string filePath, List dup ExportCollection(filePath, duplications, format); } - public static 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); - DependencyGraphGenerator.GenerateGraph(dependencies, Path.ChangeExtension(outputFilePath, ".dot")); + await DependencyGraphGenerator.GenerateGraph(dependencies, Path.ChangeExtension(outputFilePath, ".dot")); } public static void ExportMetrics(string filePath, List metrics,