diff --git a/CodeLineCounter.Tests/CodeAnalyzerTests.cs b/CodeLineCounter.Tests/CodeAnalyzerTests.cs index a40138c..5598ebd 100644 --- a/CodeLineCounter.Tests/CodeAnalyzerTests.cs +++ b/CodeLineCounter.Tests/CodeAnalyzerTests.cs @@ -12,7 +12,7 @@ public void TestAnalyzeSolution() var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..", "CodeLineCounter.sln")); // Act - var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = CodeMetricsAnalyzer.AnalyzeSolution(solutionPath); + var (metrics, projectTotals, totalLines, totalFiles, duplicationMap, dependencies) = CodeMetricsAnalyzer.AnalyzeSolution(solutionPath); // Assert Assert.NotNull(metrics); @@ -21,6 +21,7 @@ public void TestAnalyzeSolution() Assert.NotEqual(0, totalLines); Assert.NotEqual(0, totalFiles); Assert.NotNull(duplicationMap); + Assert.NotNull(dependencies); } [Fact] diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index 6be809d..e39eadf 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -210,6 +210,51 @@ public void get_file_duplications_count_uses_current_dir_for_null_solution_path( Assert.Equal(4, result); } + // Successfully exports dependencies to specified format (CSV/JSON) and creates DOT file + [Fact] + 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 }, + }; + + var testFilePath = "test_export"; + var format = CoreUtils.ExportFormat.JSON; + + // Act + DataExporter.ExportDependencies(testFilePath, dependencies, format); + + // Assert + string expectedJsonPath = CoreUtils.GetExportFileNameWithExtension(testFilePath, format); + string expectedDotPath = Path.ChangeExtension(expectedJsonPath, ".dot"); + + Assert.True(File.Exists(expectedJsonPath)); + Assert.True(File.Exists(expectedDotPath)); + + try + { + if (File.Exists(expectedJsonPath)) + { + File.Delete(expectedJsonPath); + } + + if (File.Exists(expectedDotPath)) + { + File.Delete(expectedDotPath); + } + } + catch (IOException ex) + { + throw new IOException($"Error deleting files: {ex.Message}", ex); + } + catch (UnauthorizedAccessException ex) + { + throw new UnauthorizedAccessException($"Access denied while deleting files: {ex.Message}", ex); + } + } + protected virtual void Dispose(bool disposing) { if (!_disposed) diff --git a/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs new file mode 100644 index 0000000..401ad57 --- /dev/null +++ b/CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs @@ -0,0 +1,59 @@ +using CodeLineCounter.Utils; +using CodeLineCounter.Models; +using CodeLineCounter.Services; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace CodeLineCounter.Tests +{ + public class DependencyGraphGeneratorTests + { + [Fact] + 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} + }; + + string outputPath = Path.Combine(Path.GetTempPath(), "test_graph.dot"); + + // Act + DependencyGraphGenerator.GenerateGraph(dependencies, outputPath); + + // Assert + Assert.True(File.Exists(outputPath)); + string content = File.ReadAllText(outputPath); + Assert.Contains("ClassA", content); + Assert.Contains("ClassB", content); + Assert.Contains("ClassC", content); + + File.Delete(outputPath); + } + + // Empty dependencies list + [Fact] + public void 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); + + // Assert + Assert.True(File.Exists(outputPath)); + string content = File.ReadAllText(outputPath); + Assert.Contains("digraph", content); + Assert.DoesNotContain("->", content); + + File.Delete(outputPath); + } + + + } +} diff --git a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs index 3468569..bdd665e 100644 --- a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -14,6 +14,12 @@ public void PerformAnalysis_ShouldReturnCorrectAnalysisResult() var basePath = FileUtils.GetBasePath(); var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); solutionPath = Path.Combine(solutionPath, "CodeLineCounter.sln"); + var sw = new StringWriter(); + Console.SetOut(sw); + Console.WriteLine($"Constructed solution path: {solutionPath}"); + Assert.True(File.Exists(solutionPath), $"The solution file '{solutionPath}' does not exist."); + Console.WriteLine($"Constructed solution path: {solutionPath}"); + Assert.True(File.Exists(solutionPath), $"The solution file '{solutionPath}' does not exist."); // Act var result = SolutionAnalyzer.PerformAnalysis(solutionPath); @@ -34,6 +40,7 @@ public void OutputAnalysisResults_ShouldPrintCorrectOutput() TotalLines = 1000, TotalFiles = 10, DuplicationMap = new List(), + DependencyList = new List(), ProcessingTime = TimeSpan.FromSeconds(10), SolutionFileName = "CodeLineCounter.sln", DuplicatedLines = 100 @@ -109,5 +116,39 @@ public void OutputDetailedMetrics_ShouldPrintMetricsAndProjectTotals() } } + // Export metrics, duplications and dependencies data in parallel for valid input +[Fact] +public void export_results_with_valid_input_exports_all_files() +{ + // Arrange + var result = new AnalysisResult + { + SolutionFileName = "TestSolution", + Metrics = new List(), + ProjectTotals = new Dictionary(), + TotalLines = 1000, + DuplicationMap = new List(), + DependencyList = new List() + }; + + var basePath = FileUtils.GetBasePath(); + var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); + + solutionPath = Path.Combine(solutionPath, "TestSolution.sln"); + var format = CoreUtils.ExportFormat.CSV; + + // Act + using (var sw = new StringWriter()) + { + Console.SetOut(sw); + SolutionAnalyzer.ExportResults(result, solutionPath, format); + } + + // Assert + Assert.True(File.Exists("TestSolution-CodeMetrics.csv")); + Assert.True(File.Exists("TestSolution-CodeDuplications.csv")); + Assert.True(File.Exists("TestSolution-CodeDependencies.csv")); +} + } } \ No newline at end of file diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index 43e91fa..c5455e2 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -5,6 +5,7 @@ net9.0 enable enable + NU1701 @@ -14,6 +15,9 @@ + + + diff --git a/CodeLineCounter/Models/AnalysisResult.cs b/CodeLineCounter/Models/AnalysisResult.cs index ebc01a2..2bb0b21 100644 --- a/CodeLineCounter/Models/AnalysisResult.cs +++ b/CodeLineCounter/Models/AnalysisResult.cs @@ -9,6 +9,7 @@ public class AnalysisResult public int TotalLines { get; set; } public int TotalFiles { get; set; } public required List DuplicationMap { get; set; } + public required List DependencyList { get; set; } public TimeSpan ProcessingTime { get; set; } public required string SolutionFileName { get; set; } public int DuplicatedLines { get; set; } diff --git a/CodeLineCounter/Models/DependencyRelation.cs b/CodeLineCounter/Models/DependencyRelation.cs new file mode 100644 index 0000000..284dafe --- /dev/null +++ b/CodeLineCounter/Models/DependencyRelation.cs @@ -0,0 +1,34 @@ +using CsvHelper.Configuration.Attributes; +namespace CodeLineCounter.Models +{ + public class DependencyRelation + { + [Name("SourceClass")] + public required string SourceClass { get; set; } + + [Name("TargetClass")] + public required string TargetClass { get; set; } + + [Name("FilePath")] + public required string FilePath { get; set; } + + [Name("StartLine")] + public int StartLine { get; set; } + + public override bool Equals(object? obj) + { + if (obj is not DependencyRelation other) + return false; + + return SourceClass == other.SourceClass && + TargetClass == other.TargetClass && + FilePath == other.FilePath && + StartLine == other.StartLine; + } + + public override int GetHashCode() + { + return HashCode.Combine(SourceClass, TargetClass, FilePath, StartLine); + } + } +} diff --git a/CodeLineCounter/Services/CodeMetricsAnalyzer.cs b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs index 3c94664..f8cddbe 100644 --- a/CodeLineCounter/Services/CodeMetricsAnalyzer.cs +++ b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs @@ -7,7 +7,7 @@ namespace CodeLineCounter.Services { public static class CodeMetricsAnalyzer { - public static (List, Dictionary, int, int, List) AnalyzeSolution(string solutionFilePath) + public static (List, Dictionary, int, int, List, List) AnalyzeSolution(string solutionFilePath) { string solutionDirectory = Path.GetDirectoryName(solutionFilePath) ?? string.Empty; var projectFiles = FileUtils.GetProjectFiles(solutionFilePath); @@ -15,18 +15,22 @@ public static (List, Dictionary, int, int, List(); var projectTotals = new Dictionary(); var codeDuplicationChecker = new CodeDuplicationChecker(); + int totalLines = 0; int totalFilesAnalyzed = 0; Parallel.Invoke( () => AnalyzeAllProjects(solutionDirectory, projectFiles, namespaceMetrics, projectTotals, ref totalLines, ref totalFilesAnalyzed), - () => codeDuplicationChecker.DetectCodeDuplicationInFiles(FileUtils.GetAllCsFiles(solutionDirectory)) + () => codeDuplicationChecker.DetectCodeDuplicationInFiles(FileUtils.GetAllCsFiles(solutionDirectory)), + () => DependencyAnalyzer.AnalyzeSolution(solutionFilePath) ); var duplicationMap = codeDuplicationChecker.GetCodeDuplicationMap(); var duplicationList = duplicationMap.Values.SelectMany(v => v).ToList(); + var dependencyList = DependencyAnalyzer.GetDependencies(); + DependencyAnalyzer.Clear(); - return (namespaceMetrics, projectTotals, totalLines, totalFilesAnalyzed, duplicationList); + return (namespaceMetrics, projectTotals, totalLines, totalFilesAnalyzed, duplicationList, dependencyList); } private static void AnalyzeAllProjects(string solutionDirectory, List projectFiles, List namespaceMetrics, Dictionary projectTotals, ref int totalLines, ref int totalFilesAnalyzed) diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs new file mode 100644 index 0000000..c54e4b7 --- /dev/null +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -0,0 +1,352 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using CodeLineCounter.Models; +using CodeLineCounter.Utils; +using System.Collections.Concurrent; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + + +namespace CodeLineCounter.Services +{ + public static class DependencyAnalyzer + { + private static readonly ConcurrentDictionary> _dependencyMap = new(); + private static readonly HashSet _solutionClasses = new(); + private static readonly object _dependencyLock = new(); + + public static void AnalyzeSolution(string solutionFilePath) + { + var projectFiles = FileUtils.GetProjectFiles(solutionFilePath); + CollectAllClasses(projectFiles); + + AnalyzeProjects(projectFiles); + } + + private static void CollectAllClasses(IEnumerable projectFiles) + { + foreach (var projectFile in projectFiles) + { + string projectDirectory = Path.GetDirectoryName(projectFile) ?? string.Empty; + var files = FileUtils.GetAllCsFiles(projectDirectory); + + foreach (var file in files) + { + var sourceCode = File.ReadAllText(file); + var tree = CSharpSyntaxTree.ParseText(sourceCode); + var root = tree.GetRoot(); + + var classes = root.DescendantNodes() + .OfType() + .Select(c => GetFullTypeName(c)); + + lock (_dependencyLock) + { + foreach (var className in classes) + { + _solutionClasses.Add(className); + } + } + } + } + } + + private static string GetFullTypeName(ClassDeclarationSyntax classDeclaration) + { + // First check the file-scoped namespace + var fileScopedNamespace = classDeclaration.SyntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .FirstOrDefault(); + if (fileScopedNamespace != null) + { + return $"{fileScopedNamespace.Name}.{classDeclaration.Identifier.Text}"; + } + + // Then check the regular namespace + var namespaceDeclaration = classDeclaration.Ancestors() + .OfType() + .FirstOrDefault(); + if (namespaceDeclaration != null) + { + return $"{namespaceDeclaration.Name}.{classDeclaration.Identifier.Text}"; + } + + return classDeclaration.Identifier.Text; + } + + public static void AnalyzeProjects(IEnumerable projectFiles) + { + foreach (var projectFile in projectFiles) + { + AnalyzeProject(projectFile); + } + } + + public static void AnalyzeProject(string projectPath) + { + string projectDirectory = Path.GetDirectoryName(projectPath) ?? string.Empty; + var files = FileUtils.GetAllCsFiles(projectDirectory); + + foreach (var file in files) + { + var sourceCode = File.ReadAllLines(file); + AnalyzeFile(file, string.Join(Environment.NewLine, sourceCode)); + } + } + + public static void AnalyzeFile(string filePath, string sourceCode) + { + var tree = CSharpSyntaxTree.ParseText(sourceCode); + var root = tree.GetRoot(); + + + var classes = root.DescendantNodes().OfType(); + + Parallel.ForEach(classes, classDeclaration => + { + var className = GetFullTypeName(classDeclaration); + var dependencies = ExtractDependencies(classDeclaration); + + foreach (var dependency in dependencies) + { + var relation = new DependencyRelation + { + SourceClass = GetFullTypeName(classDeclaration), + TargetClass = dependency, + FilePath = filePath, + StartLine = classDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line + }; + + _dependencyMap.AddOrUpdate(className, + new HashSet { relation }, + (key, set) => + { + lock (_dependencyLock) + { + // Check if the relation already exists + if (!set.Any(r => r.Equals(relation))) + { + set.Add(relation); + } + } + return set; + } + ); + } + }); + } + + private static IEnumerable ExtractDependencies(ClassDeclarationSyntax classDeclaration) + { + var dependencies = new HashSet(); + var usings = GetUsingsWithCurrentNamespace(classDeclaration); + + + AnalyzeInheritance(classDeclaration, usings, dependencies); + AnalyzeClassMembers(classDeclaration, usings, dependencies); + + return dependencies; + } + + private static List GetUsingsWithCurrentNamespace(ClassDeclarationSyntax classDeclaration) + { + var root = classDeclaration.SyntaxTree.GetRoot(); + var usings = (root as CompilationUnitSyntax)?.Usings + .Select(u => u.Name?.ToString()) + .Where(u => u != null) + .ToList() ?? new List(); + + var currentNamespace = classDeclaration.Ancestors() + .OfType() + .FirstOrDefault()?.Name.ToString(); + + if (currentNamespace != null) + { + usings.Add(currentNamespace); + } + + return usings; + } + + private static void AnalyzeInheritance(ClassDeclarationSyntax classDeclaration, + List usings, HashSet dependencies) + { + if (classDeclaration.BaseList == null) return; + + foreach (var baseType in classDeclaration.BaseList.Types) + { + AddDependencyIfExists(baseType.Type.ToString(), usings, dependencies); + } + } + + private static void AnalyzeClassMembers(ClassDeclarationSyntax classDeclaration, + List usings, HashSet dependencies) + { + var allNodes = classDeclaration.DescendantNodes(); + + foreach (var node in allNodes) + { + AnalyzeNode(node, usings, dependencies); + } + } + + private static void AnalyzeNode(SyntaxNode node, List usings, HashSet dependencies) + { + switch (node) + { + case FieldDeclarationSyntax field: + AddDependencyIfExists(field.Declaration.Type.ToString(), usings, dependencies); + break; + + case PropertyDeclarationSyntax property: + AddDependencyIfExists(property.Type.ToString(), usings, dependencies); + break; + + case VariableDeclarationSyntax variable: + AddDependencyIfExists(variable.Type.ToString(), usings, dependencies); + break; + + case ObjectCreationExpressionSyntax creation: + AddDependencyIfExists(creation.Type.ToString(), usings, dependencies); + break; + + case InvocationExpressionSyntax invocation: + AnalyzeInvocation(invocation, usings, dependencies); + break; + + case ParameterSyntax parameter: + AddDependencyIfExists(parameter.Type?.ToString() ?? "", usings, dependencies); + break; + + case GenericNameSyntax generic: + AnalyzeGenericType(generic, usings, dependencies); + break; + + case IdentifierNameSyntax identifier: + AddDependencyIfExists(identifier.Identifier.Text, usings, dependencies); + break; + + case MemberAccessExpressionSyntax memberAccess when !(memberAccess.Parent is InvocationExpressionSyntax): + AddDependencyIfExists(memberAccess.Expression.ToString(), usings, dependencies); + break; + } + } + + private static void AnalyzeInvocation(InvocationExpressionSyntax invocation, + List usings, HashSet dependencies) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + AddDependencyIfExists(memberAccess.Expression.ToString(), usings, dependencies); + } + } + + private static void AnalyzeGenericType(GenericNameSyntax generic, + List usings, HashSet dependencies) + { + foreach (var typeArg in generic.TypeArgumentList.Arguments) + { + AddDependencyIfExists(typeArg.ToString(), usings, dependencies); + } + } + + private static void AddDependencyIfExists(string typeName, List usings, HashSet dependencies) + { + var fullTypeName = GetFullTypeNameFromSymbol(typeName, usings); + if (_solutionClasses.Contains(fullTypeName)) + { + dependencies.Add(fullTypeName); + } + } + + private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) + { + if (string.IsNullOrEmpty(typeName)) + return typeName; + + if (!typeName.Contains("<")) + return ResolveSimpleTypeName(typeName, usings); + + return HandleGenericType(typeName, usings); + } + + private static string ResolveSimpleTypeName(string typeName, IEnumerable usings) + { + if (typeName.Contains(".")) + return typeName; + + return FindTypeInUsings(typeName, usings); + } + + private static string FindTypeInUsings(string typeName, IEnumerable usings) + { + foreach (var usingStatement in usings) + { + var fullName = $"{usingStatement}.{typeName}"; + if (_solutionClasses.Contains(fullName)) + { + return fullName; + } + } + return typeName; + } + + private static string HandleGenericType(string typeName, IEnumerable usings) + { + var parts = SplitGenericType(typeName); + return string.Join("", parts.Select(p => p.Contains("<") + ? p + : GetFullTypeNameFromSymbol(p, usings))); + } + + private static List SplitGenericType(string typeName) + { + var parts = new List(); + var depth = 0; + var lastIndex = 0; + + for (var i = 0; i < typeName.Length; i++) + { + if (typeName[i] == '<') + { + if (depth == 0) + { + parts.Add(typeName[lastIndex..i]); + lastIndex = i; + } + depth++; + } + else if (typeName[i] == '>') + { + depth--; + if (depth == 0) + { + parts.Add(typeName[lastIndex..(i + 1)]); + lastIndex = i + 1; + } + } + } + + if (lastIndex < typeName.Length) + parts.Add(typeName[lastIndex..]); + + return parts; + } + + public static List GetDependencies() + { + return _dependencyMap.SelectMany(kvp => kvp.Value).ToList() ?? new List(); + } + + 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 new file mode 100644 index 0000000..fef1817 --- /dev/null +++ b/CodeLineCounter/Services/DependencyGraphGenerator.cs @@ -0,0 +1,37 @@ +using CodeLineCounter.Models; +using QuikGraph; +using QuikGraph.Graphviz; +using System.Diagnostics; + +namespace CodeLineCounter.Services +{ + + public static class DependencyGraphGenerator + { + public static void GenerateGraph(List dependencies, string outputPath) + { + var graph = new AdjacencyGraph>(); + + foreach (var dependency in dependencies) + { + if (!graph.ContainsVertex(dependency.SourceClass)) + graph.AddVertex(dependency.SourceClass); + if (!graph.ContainsVertex(dependency.TargetClass)) + graph.AddVertex(dependency.TargetClass); + + graph.AddEdge(new Edge(dependency.SourceClass, dependency.TargetClass)); + } + + var graphviz = new GraphvizAlgorithm>(graph); + graphviz.FormatVertex += (sender, args) => + { + args.VertexFormat.Label = args.Vertex; + args.VertexFormat.Shape = QuikGraph.Graphviz.Dot.GraphvizVertexShape.Box; + }; + + string dot = graphviz.Generate(); + File.WriteAllText(outputPath, dot); + + } + } +} \ No newline at end of file diff --git a/CodeLineCounter/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs index 9d3d53b..df3c579 100644 --- a/CodeLineCounter/Services/SolutionAnalyzer.cs +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -15,6 +15,7 @@ public static void AnalyzeAndExportSolution(string solutionPath, bool verbose, C var analysisResult = PerformAnalysis(solutionPath); OutputAnalysisResults(analysisResult, verbose); ExportResults(analysisResult, solutionPath, format); + } catch (Exception ex) { @@ -28,7 +29,7 @@ public static AnalysisResult PerformAnalysis(string solutionPath) var timer = new Stopwatch(); timer.Start(); - var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = + var (metrics, projectTotals, totalLines, totalFiles, duplicationMap, dependencyList) = CodeMetricsAnalyzer.AnalyzeSolution(solutionPath); timer.Stop(); @@ -40,6 +41,7 @@ public static AnalysisResult PerformAnalysis(string solutionPath) TotalLines = totalLines, TotalFiles = totalFiles, DuplicationMap = duplicationMap, + DependencyList = dependencyList, ProcessingTime = timer.Elapsed, SolutionFileName = Path.GetFileName(solutionPath), DuplicatedLines = duplicationMap.Sum(x => x.NbLines) @@ -69,6 +71,8 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor $"{result.SolutionFileName}-CodeMetrics.xxx", format); var duplicationOutputFilePath = CoreUtils.GetExportFileNameWithExtension( $"{result.SolutionFileName}-CodeDuplications.xxx", format); + var dependenciesOutputFilePath = CoreUtils.GetExportFileNameWithExtension( + $"{result.SolutionFileName}-CodeDependencies.xxx", format); try { @@ -84,11 +88,16 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor () => DataExporter.ExportDuplications( duplicationOutputFilePath, result.DuplicationMap, + format), + () => DataExporter.ExportDependencies( + dependenciesOutputFilePath, + result.DependencyList, format) ); Console.WriteLine($"The data has been exported to {metricsOutputFilePath}"); Console.WriteLine($"The code duplications have been exported to {duplicationOutputFilePath}"); + Console.WriteLine($"The code dependencies have been exported to {dependenciesOutputFilePath} and the graph has been generated. (dot file can be found in the same directory)"); } catch (AggregateException ae) { diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index 34a0eae..412af07 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -1,4 +1,6 @@ using CodeLineCounter.Models; +using CodeLineCounter.Services; + namespace CodeLineCounter.Utils { @@ -13,20 +15,10 @@ public static class DataExporter public static void Export(string filePath, T data, CoreUtils.ExportFormat format) where T : class { - if (string.IsNullOrEmpty(filePath)) - throw new ArgumentException("File path cannot be null or empty", nameof(filePath)); if (data == null) throw new ArgumentNullException(nameof(data)); - try - { - filePath = CoreUtils.GetExportFileNameWithExtension(filePath, format); - _exportStrategies[format].Export(filePath, new List { data }); - } - catch (IOException ex) - { - throw new IOException($"Failed to export data to {filePath}", ex); - } + ExportCollection(filePath, new List { data }, format); } public static void ExportCollection(string filePath, IEnumerable data, CoreUtils.ExportFormat format) where T : class @@ -52,6 +44,14 @@ public static void ExportDuplications(string filePath, List dup ExportCollection(filePath, duplications, format); } + public static 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")); + } + public static void ExportMetrics(string filePath, List metrics, Dictionary projectTotals, int totalLines, List duplications, string? solutionPath, CoreUtils.ExportFormat format) diff --git a/CodeLineCounter/Utils/FileUtils.cs b/CodeLineCounter/Utils/FileUtils.cs index 854d6a0..f34a95b 100644 --- a/CodeLineCounter/Utils/FileUtils.cs +++ b/CodeLineCounter/Utils/FileUtils.cs @@ -13,11 +13,21 @@ public static List GetAllCsFiles(string rootPath) public static List GetSolutionFiles(string rootPath) { - return [.. Directory.GetFiles(rootPath, "*.sln", SearchOption.TopDirectoryOnly)]; + if (!Directory.Exists(rootPath)) + { + throw new UnauthorizedAccessException($"Access to the path '{rootPath}' is denied."); + } + + return Directory.GetFiles(rootPath, "*.sln", SearchOption.TopDirectoryOnly).ToList(); } public static List GetProjectFiles(string solutionFilePath) { + if (!File.Exists(solutionFilePath)) + { + throw new UnauthorizedAccessException($"Access to the path '{solutionFilePath}' is denied."); + } + var projectFiles = new List(); var lines = File.ReadAllLines(solutionFilePath); diff --git a/README.md b/README.md index 1eb70d0..90ea9bd 100644 --- a/README.md +++ b/README.md @@ -11,25 +11,31 @@ The `CodeLineCounter` project is a tool that counts the number of lines of code per file, namespace, and project in a .NET solution. It also calculates the cyclomatic complexity of each file. Since version `1.0.2` it also check duplications in the code. +Since version `1.2.0` it also generate duplication report. -All the results are exported to CSV or JSON files. +All the results are exported to CSV or JSON files. A Graph of dependency is also generated. (dot file Graphviz format) ## Features - Counts the number of lines of code per file, namespace, and project. - Calculates the cyclomatic complexity of each file. +- Calculates the number of code duplications in the code. +- Calculates and generate a graph of dependencies. - Exports the results to a CSV or JSON file. ## Prerequisites - .NET 9.0 SDK installed. -- The following NuGet packages: +- The following NuGet packages (use dotnet restore): - `Microsoft.CodeAnalysis.CSharp` + - `coverlet.msbuild` + - `CsvHelper` + - `QuikGraph.Graphviz` + - `Graphviz.Net` - `coverlet.collector` - `Microsoft.NET.Test.Sdk` - `xunit` - `xunit.runner.visualstudio` - - `coverlet.collector` ## Installation @@ -128,7 +134,7 @@ The program generates a CSV file named `-CodeDuplication.csv` in t ```csv Code Hash,FilePath,MethodName,StartLine,NbLines -0133e750c0fec3d478670cb0441882855926c415a35aacf0360508fdeb73c34c,C:\temp\NamespaceMetrics.cs,CodeLineCounter\Models\class.cs,EtablirCommunication,91,3 +0133e750c0fec3d478670cb0441882855926c415a35aacf0360508fdeb73c34c,C:\temp\NamespaceMetrics.cs,CodeLineCounter\Models\class.cs,OpenCommunication,91,3 ``` ## Example Output of CodeDuplication.json @@ -138,7 +144,7 @@ Code Hash,FilePath,MethodName,StartLine,NbLines { "CodeHash": "0133e750c0fec3d478670cb0441882855926c415a35aacf0360508fdeb73c34c", "FilePath": "C:\\temp\\NamespaceMetrics.cs", - "MethodName": "EtablirCommunication", + "MethodName": "OpenCommunication", "StartLine": 91, "NbLines": 3 }, @@ -160,12 +166,15 @@ CodeLineCounter/ ├── CodeLineCounter/ │ ├── Models/ │ │ └── AnalysisResult.cs +│ │ └── Dependencies.cs │ │ └── DuplicationCode.cs │ │ └── NamespaceMetrics.cs │ ├── Services/ │ │ ├── CodeMetricsAnalyzer.cs │ │ ├── CodeDuplicationChecker.cs │ │ └── CyclomaticComplexityCalculator.cs +│ │ └── DependencyAnalyzer.cs +│ │ └── DependencyGraphGenerator.cs │ │ └── SolutionAnalyzer.cs │ ├── Utils/ │ │ ├── CoreUtils.cs @@ -183,6 +192,7 @@ CodeLineCounter/ │ ├── CyclomaticComplexityCalculatorTests.cs │ ├── CoreUtilsTests.cs │ ├── DataExporterTests.cs +│ ├── DependencyGraphGeneratorTests.cs │ ├── CsvHandlerTests.cs │ ├── FileUtilsTests.cs │ ├── HashUtilsTests.cs