From e9a4551bcc72f5ccfee0404b33aa785d9830e64c Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 18:16:29 +0100 Subject: [PATCH 01/13] feat: add dependency analysis and export functionality --- CodeLineCounter.Tests/CodeAnalyzerTests.cs | 3 +- .../SolutionAnalyzerTests.cs | 5 + CodeLineCounter/CodeLineCounter.csproj | 6 + CodeLineCounter/Models/AnalysisResult.cs | 1 + CodeLineCounter/Models/DependencyRelation.cs | 34 +++ .../Services/CodeMetricsAnalyzer.cs | 9 +- .../Services/DependencyAnalyzer.cs | 268 ++++++++++++++++++ .../Services/DependencyGraphGenerator.cs | 37 +++ CodeLineCounter/Services/SolutionAnalyzer.cs | 9 +- CodeLineCounter/Utils/DataExporter.cs | 10 + CodeLineCounter/Utils/FileUtils.cs | 12 +- 11 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 CodeLineCounter/Models/DependencyRelation.cs create mode 100644 CodeLineCounter/Services/DependencyAnalyzer.cs create mode 100644 CodeLineCounter/Services/DependencyGraphGenerator.cs 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/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs index 3468569..fc4d74c 100644 --- a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -14,6 +14,10 @@ public void PerformAnalysis_ShouldReturnCorrectAnalysisResult() var basePath = FileUtils.GetBasePath(); var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); solutionPath = Path.Combine(solutionPath, "CodeLineCounter.sln"); + 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 +38,7 @@ public void OutputAnalysisResults_ShouldPrintCorrectOutput() TotalLines = 1000, TotalFiles = 10, DuplicationMap = new List(), + DependencyList = new List(), ProcessingTime = TimeSpan.FromSeconds(10), SolutionFileName = "CodeLineCounter.sln", DuplicatedLines = 100 diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index 43e91fa..6437bcb 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -13,7 +13,13 @@ all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 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..ac1ca8c --- /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..4c8187c 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,21 @@ 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(); - 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..fe3073a --- /dev/null +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -0,0 +1,268 @@ +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 class DependencyAnalyzer + { + private static readonly ConcurrentDictionary> _dependencyMap = new(); + private static readonly object _dependencyLock = new(); + + public static void AnalyzeSolution(string solutionFilePath) + { + var projectFiles = FileUtils.GetProjectFiles(solutionFilePath); + + AnalyzeProjects(projectFiles); + } + + 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 compilation = root as CompilationUnitSyntax; + var usings = compilation?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); + + var classes = root.DescendantNodes().OfType(); + + Parallel.ForEach(classes, classDeclaration => + { + var className = classDeclaration.Identifier.Text; + var dependencies = ExtractDependencies(classDeclaration); + + foreach (var dependency in dependencies) + { + var relation = new DependencyRelation + { + SourceClass = className, + TargetClass = dependency, + FilePath = filePath, + StartLine = classDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line + }; + + _dependencyMap.AddOrUpdate(className, + new HashSet { relation }, + (key, set) => + { + lock (_dependencyLock) + { + // Vérifier si la relation existe déjà + if (!set.Any(r => r.Equals(relation))) + { + set.Add(relation); + } + } + return set; + } + ); + } + }); + } + + private static IEnumerable ExtractDependencies(ClassDeclarationSyntax classDeclaration) + { + var dependencies = new HashSet(); + var typesToExclude = new HashSet + { + // System Object + "object", + "Object", + "System.Object", + + // Value Types + "bool", + "Boolean", + "System.Boolean", + "byte", + "Byte", + "System.Byte", + "sbyte", + "SByte", + "System.SByte", + "char", + "Char", + "System.Char", + "decimal", + "Decimal", + "System.Decimal", + "double", + "Double", + "System.Double", + "float", + "Single", + "System.Single", + "int", + "Int32", + "System.Int32", + "uint", + "UInt32", + "System.UInt32", + "long", + "Int64", + "System.Int64", + "ulong", + "UInt64", + "System.UInt64", + "short", + "Int16", + "System.Int16", + "ushort", + "UInt16", + "System.UInt16", + + // String + "string", + "String", + "System.String", + + // Common Base Types + "ValueType", + "System.ValueType", + "Enum", + "System.Enum", + "Delegate", + "System.Delegate", + "MulticastDelegate", + "System.MulticastDelegate", + + // Common System Types + "void", + "Void", + "System.Void", + "DateTime", + "System.DateTime", + "TimeSpan", + "System.TimeSpan", + "Guid", + "System.Guid", + + // Common Collections + "Array", + "System.Array", + "IEnumerable", + "System.Collections.IEnumerable", + "IEnumerable<>", + "System.Collections.Generic.IEnumerable<>", + "ICollection", + "System.Collections.ICollection", + "ICollection<>", + "System.Collections.Generic.ICollection<>", + "IList", + "System.Collections.IList", + "IList<>", + "System.Collections.Generic.IList<>", + "List<>", + "System.Collections.Generic.List<>", + "IDictionary", + "System.Collections.IDictionary", + "IDictionary<,>", + "System.Collections.Generic.IDictionary<,>", + "Dictionary<,>", + "System.Collections.Generic.Dictionary<,>", + + // Task Types + "Task", + "System.Threading.Tasks.Task", + "Task<>", + "System.Threading.Tasks.Task<>", + "ValueTask", + "System.Threading.Tasks.ValueTask", + "ValueTask<>", + "System.Threading.Tasks.ValueTask<>" + }; + + + + // Analyze inheritance + if (classDeclaration.BaseList != null) + { + foreach (var baseType in classDeclaration.BaseList.Types) + { + var typeName = baseType.Type.ToString(); + if (!typesToExclude.Contains(typeName)) + { + dependencies.Add(typeName); + } + } + } + + // Rest of the method remains the same + foreach (var member in classDeclaration.Members) + { + /*if (member is FieldDeclarationSyntax field) + { + foreach (var variable in field.Declaration.Variables) + { + var typeName = field.Declaration.Type.ToString(); + if (!typesToExclude.Contains(typeName)) + { + dependencies.Add(typeName); + } + } + } + else */ + if (member is PropertyDeclarationSyntax property) + { + var typeName = property.Type.ToString(); + if (!typesToExclude.Contains(typeName)) + { + dependencies.Add(typeName); + } + } + else if (member is MethodDeclarationSyntax method) + { + var returnType = method.ReturnType.ToString(); + if (!typesToExclude.Contains(returnType)) + { + dependencies.Add(returnType); + } + + foreach (var parameter in method.ParameterList.Parameters) + { + var paramType = parameter.Type.ToString(); + if (!typesToExclude.Contains(paramType)) + { + dependencies.Add(paramType); + } + } + } + } + + return dependencies; + } + + public static List GetDependencies() + { + return _dependencyMap.SelectMany(kvp => kvp.Value).ToList() ?? new List(); + } + } +} \ 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..b3d7390 100644 --- a/CodeLineCounter/Services/SolutionAnalyzer.cs +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -28,7 +28,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 +40,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 +70,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,6 +87,10 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor () => DataExporter.ExportDuplications( duplicationOutputFilePath, result.DuplicationMap, + format), + () => DataExporter.ExportDependencies( + dependenciesOutputFilePath, + result.DependencyList, format) ); diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index 34a0eae..2de7faf 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -1,4 +1,6 @@ using CodeLineCounter.Models; +using CodeLineCounter.Services; + namespace CodeLineCounter.Utils { @@ -52,6 +54,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); From 0bf57b8af872c57e1016d743e6927037110cd218 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 19:25:33 +0100 Subject: [PATCH 02/13] feat: improve dependency analysis with full type name resolution --- .../Services/DependencyAnalyzer.cs | 318 ++++++++++-------- 1 file changed, 173 insertions(+), 145 deletions(-) diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index fe3073a..f7ee0ee 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -16,15 +16,69 @@ namespace CodeLineCounter.Services public 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) + { + // Vérifier d'abord le namespace filescoped + var fileScopedNamespace = classDeclaration.SyntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .FirstOrDefault(); + if (fileScopedNamespace != null) + { + return $"{fileScopedNamespace.Name}.{classDeclaration.Identifier.Text}"; + } + + // Vérifier ensuite le namespace classique + 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) @@ -56,14 +110,14 @@ public static void AnalyzeFile(string filePath, string sourceCode) Parallel.ForEach(classes, classDeclaration => { - var className = classDeclaration.Identifier.Text; + var className = GetFullTypeName(classDeclaration); var dependencies = ExtractDependencies(classDeclaration); foreach (var dependency in dependencies) { var relation = new DependencyRelation { - SourceClass = className, + SourceClass = GetFullTypeName(classDeclaration), TargetClass = dependency, FilePath = filePath, StartLine = classDeclaration.GetLocation().GetLineSpan().StartLinePosition.Line @@ -91,175 +145,149 @@ public static void AnalyzeFile(string filePath, string sourceCode) private static IEnumerable ExtractDependencies(ClassDeclarationSyntax classDeclaration) { var dependencies = new HashSet(); - var typesToExclude = new HashSet - { - // System Object - "object", - "Object", - "System.Object", - - // Value Types - "bool", - "Boolean", - "System.Boolean", - "byte", - "Byte", - "System.Byte", - "sbyte", - "SByte", - "System.SByte", - "char", - "Char", - "System.Char", - "decimal", - "Decimal", - "System.Decimal", - "double", - "Double", - "System.Double", - "float", - "Single", - "System.Single", - "int", - "Int32", - "System.Int32", - "uint", - "UInt32", - "System.UInt32", - "long", - "Int64", - "System.Int64", - "ulong", - "UInt64", - "System.UInt64", - "short", - "Int16", - "System.Int16", - "ushort", - "UInt16", - "System.UInt16", - - // String - "string", - "String", - "System.String", - - // Common Base Types - "ValueType", - "System.ValueType", - "Enum", - "System.Enum", - "Delegate", - "System.Delegate", - "MulticastDelegate", - "System.MulticastDelegate", - - // Common System Types - "void", - "Void", - "System.Void", - "DateTime", - "System.DateTime", - "TimeSpan", - "System.TimeSpan", - "Guid", - "System.Guid", - - // Common Collections - "Array", - "System.Array", - "IEnumerable", - "System.Collections.IEnumerable", - "IEnumerable<>", - "System.Collections.Generic.IEnumerable<>", - "ICollection", - "System.Collections.ICollection", - "ICollection<>", - "System.Collections.Generic.ICollection<>", - "IList", - "System.Collections.IList", - "IList<>", - "System.Collections.Generic.IList<>", - "List<>", - "System.Collections.Generic.List<>", - "IDictionary", - "System.Collections.IDictionary", - "IDictionary<,>", - "System.Collections.Generic.IDictionary<,>", - "Dictionary<,>", - "System.Collections.Generic.Dictionary<,>", - - // Task Types - "Task", - "System.Threading.Tasks.Task", - "Task<>", - "System.Threading.Tasks.Task<>", - "ValueTask", - "System.Threading.Tasks.ValueTask", - "ValueTask<>", - "System.Threading.Tasks.ValueTask<>" - }; - - + var root = classDeclaration.SyntaxTree.GetRoot(); + var usings = (root as CompilationUnitSyntax)?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); // Analyze inheritance if (classDeclaration.BaseList != null) { foreach (var baseType in classDeclaration.BaseList.Types) { - var typeName = baseType.Type.ToString(); - if (!typesToExclude.Contains(typeName)) + var typeName = GetFullTypeNameFromSymbol(baseType.Type.ToString(), usings); + if (_solutionClasses.Contains(typeName)) { dependencies.Add(typeName); } } } - // Rest of the method remains the same - foreach (var member in classDeclaration.Members) + // Analyze all nodes in the class + var allNodes = classDeclaration.DescendantNodes(); + + foreach (var node in allNodes) { - /*if (member is FieldDeclarationSyntax field) + switch (node) { - foreach (var variable in field.Declaration.Variables) - { - var typeName = field.Declaration.Type.ToString(); - if (!typesToExclude.Contains(typeName)) + case FieldDeclarationSyntax field: + var fieldType = GetFullTypeNameFromSymbol(field.Declaration.Type.ToString(), usings); + if (_solutionClasses.Contains(fieldType)) { - dependencies.Add(typeName); + dependencies.Add(fieldType); } - } - } - else */ - if (member is PropertyDeclarationSyntax property) - { - var typeName = property.Type.ToString(); - if (!typesToExclude.Contains(typeName)) - { - dependencies.Add(typeName); - } - } - else if (member is MethodDeclarationSyntax method) - { - var returnType = method.ReturnType.ToString(); - if (!typesToExclude.Contains(returnType)) - { - dependencies.Add(returnType); - } + break; - foreach (var parameter in method.ParameterList.Parameters) - { - var paramType = parameter.Type.ToString(); - if (!typesToExclude.Contains(paramType)) + case PropertyDeclarationSyntax property: + var propertyType = GetFullTypeNameFromSymbol(property.Type.ToString(), usings); + if (_solutionClasses.Contains(propertyType)) + { + dependencies.Add(propertyType); + } + break; + + case VariableDeclarationSyntax variable: + var varType = GetFullTypeNameFromSymbol(variable.Type.ToString(), usings); + if (_solutionClasses.Contains(varType)) + { + dependencies.Add(varType); + } + break; + + case ObjectCreationExpressionSyntax creation: + var creationType = GetFullTypeNameFromSymbol(creation.Type.ToString(), usings); + if (_solutionClasses.Contains(creationType)) + { + dependencies.Add(creationType); + } + break; + + case InvocationExpressionSyntax invocation: + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + var expressionType = GetFullTypeNameFromSymbol(memberAccess.Expression.ToString(), usings); + if (_solutionClasses.Contains(expressionType)) + { + dependencies.Add(expressionType); + } + } + break; + + case ParameterSyntax parameter: + var paramType = GetFullTypeNameFromSymbol(parameter.Type?.ToString() ?? "", usings); + if (_solutionClasses.Contains(paramType)) { dependencies.Add(paramType); } - } + break; + + case GenericNameSyntax generic: + foreach (var typeArg in generic.TypeArgumentList.Arguments) + { + var genericType = GetFullTypeNameFromSymbol(typeArg.ToString(), usings); + if (_solutionClasses.Contains(genericType)) + { + dependencies.Add(genericType); + } + } + break; + case IdentifierNameSyntax identifier: + var identifierType = GetFullTypeNameFromSymbol(identifier.Identifier.Text, usings); + if (_solutionClasses.Contains(identifierType)) + { + dependencies.Add(identifierType); + } + break; + + case MemberAccessExpressionSyntax innerMemberAccess when !(innerMemberAccess.Parent is InvocationExpressionSyntax): + var memberType = GetFullTypeNameFromSymbol(innerMemberAccess.Expression.ToString(), usings); + if (_solutionClasses.Contains(memberType)) + { + dependencies.Add(memberType); + } + break; } } return dependencies; } + private static IEnumerable GetUsings(SyntaxNode node) + { + var root = node.SyntaxTree.GetRoot(); + var compilation = root as CompilationUnitSyntax; + return compilation?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); + } + + private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) + { + // Gérer les types génériques + if (typeName.Contains("<")) + { + var baseType = typeName.Substring(0, typeName.IndexOf("<")); + var genericPart = typeName.Substring(typeName.IndexOf("<")); + + // Résoudre le type de base + var resolvedBaseType = GetFullTypeNameFromSymbol(baseType, usings); + return resolvedBaseType + genericPart; + } + + // Si le type contient déjà un namespace, on le retourne tel quel + if (typeName.Contains(".")) + return typeName; + + // Chercher dans les using si on trouve le namespace correspondant + foreach (var usingStatement in usings) + { + var fullName = $"{usingStatement}.{typeName}"; + if (_solutionClasses.Contains(fullName)) + { + return fullName; + } + } + + // Vérifier dans le namespace courant + return typeName; + } + public static List GetDependencies() { return _dependencyMap.SelectMany(kvp => kvp.Value).ToList() ?? new List(); From eb43ac46f5924f9fe02e9e4b44f308a6ec4be828 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 19:31:12 +0100 Subject: [PATCH 03/13] feat: improve dependency analysis with nested generics and namespace handling --- .../Services/DependencyAnalyzer.cs | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index f7ee0ee..3d608ab 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -146,7 +146,18 @@ private static IEnumerable ExtractDependencies(ClassDeclarationSyntax cl { var dependencies = new HashSet(); var root = classDeclaration.SyntaxTree.GetRoot(); - var usings = (root as CompilationUnitSyntax)?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); + // Récupérer les usings une seule fois au début + var usings = (root as CompilationUnitSyntax)?.Usings.Select(u => u.Name.ToString()).ToList() + ?? new List(); + + // Ajouter le namespace courant aux usings si présent + var currentNamespace = classDeclaration.Ancestors() + .OfType() + .FirstOrDefault()?.Name.ToString(); + if (currentNamespace != null) + { + usings.Add(currentNamespace); + } // Analyze inheritance if (classDeclaration.BaseList != null) @@ -259,15 +270,44 @@ private static IEnumerable GetUsings(SyntaxNode node) private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) { - // Gérer les types génériques + if (string.IsNullOrEmpty(typeName)) + return typeName; + + // Gérer les types génériques imbriqués if (typeName.Contains("<")) { - var baseType = typeName.Substring(0, typeName.IndexOf("<")); - var genericPart = typeName.Substring(typeName.IndexOf("<")); + var depth = 0; + var lastIndex = 0; + var parts = new List(); - // Résoudre le type de base - var resolvedBaseType = GetFullTypeNameFromSymbol(baseType, usings); - return resolvedBaseType + genericPart; + 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 string.Join("", parts.Select(p => p.Contains("<") + ? p + : GetFullTypeNameFromSymbol(p, usings))); } // Si le type contient déjà un namespace, on le retourne tel quel @@ -292,5 +332,11 @@ 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 From e15aad7f34c492a4ef0ea78e64f16d0899590e87 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 19:51:38 +0100 Subject: [PATCH 04/13] feat: add tests for dependency export and graph generation functionality --- CodeLineCounter.Tests/DataExporterTests.cs | 27 ++ .../DependencyGraphGeneratorTests.cs | 59 ++++ .../Services/DependencyAnalyzer.cs | 271 +++++++++--------- 3 files changed, 226 insertions(+), 131 deletions(-) create mode 100644 CodeLineCounter.Tests/DependencyGraphGeneratorTests.cs diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index 6be809d..d206b88 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -210,6 +210,33 @@ 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)); + + File.Delete(expectedJsonPath); + File.Delete(expectedDotPath); + } + 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/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index 3d608ab..0cda23b 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -145,127 +145,122 @@ public static void AnalyzeFile(string filePath, string sourceCode) 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(); - // Récupérer les usings une seule fois au début - var usings = (root as CompilationUnitSyntax)?.Usings.Select(u => u.Name.ToString()).ToList() - ?? new List(); + var usings = (root as CompilationUnitSyntax)?.Usings + .Select(u => u.Name.ToString()) + .ToList() ?? new List(); - // Ajouter le namespace courant aux usings si présent var currentNamespace = classDeclaration.Ancestors() .OfType() .FirstOrDefault()?.Name.ToString(); + if (currentNamespace != null) { usings.Add(currentNamespace); } - // Analyze inheritance - if (classDeclaration.BaseList != null) + return usings; + } + + private static void AnalyzeInheritance(ClassDeclarationSyntax classDeclaration, + List usings, HashSet dependencies) + { + if (classDeclaration.BaseList == null) return; + + foreach (var baseType in classDeclaration.BaseList.Types) { - foreach (var baseType in classDeclaration.BaseList.Types) - { - var typeName = GetFullTypeNameFromSymbol(baseType.Type.ToString(), usings); - if (_solutionClasses.Contains(typeName)) - { - dependencies.Add(typeName); - } - } + AddDependencyIfExists(baseType.Type.ToString(), usings, dependencies); } + } - // Analyze all nodes in the class + private static void AnalyzeClassMembers(ClassDeclarationSyntax classDeclaration, + List usings, HashSet dependencies) + { var allNodes = classDeclaration.DescendantNodes(); foreach (var node in allNodes) { - switch (node) - { - case FieldDeclarationSyntax field: - var fieldType = GetFullTypeNameFromSymbol(field.Declaration.Type.ToString(), usings); - if (_solutionClasses.Contains(fieldType)) - { - dependencies.Add(fieldType); - } - break; + AnalyzeNode(node, usings, dependencies); + } + } - case PropertyDeclarationSyntax property: - var propertyType = GetFullTypeNameFromSymbol(property.Type.ToString(), usings); - if (_solutionClasses.Contains(propertyType)) - { - dependencies.Add(propertyType); - } - break; + private static void AnalyzeNode(SyntaxNode node, List usings, HashSet dependencies) + { + switch (node) + { + case FieldDeclarationSyntax field: + AddDependencyIfExists(field.Declaration.Type.ToString(), usings, dependencies); + break; - case VariableDeclarationSyntax variable: - var varType = GetFullTypeNameFromSymbol(variable.Type.ToString(), usings); - if (_solutionClasses.Contains(varType)) - { - dependencies.Add(varType); - } - break; + case PropertyDeclarationSyntax property: + AddDependencyIfExists(property.Type.ToString(), usings, dependencies); + break; - case ObjectCreationExpressionSyntax creation: - var creationType = GetFullTypeNameFromSymbol(creation.Type.ToString(), usings); - if (_solutionClasses.Contains(creationType)) - { - dependencies.Add(creationType); - } - break; + case VariableDeclarationSyntax variable: + AddDependencyIfExists(variable.Type.ToString(), usings, dependencies); + break; - case InvocationExpressionSyntax invocation: - if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) - { - var expressionType = GetFullTypeNameFromSymbol(memberAccess.Expression.ToString(), usings); - if (_solutionClasses.Contains(expressionType)) - { - dependencies.Add(expressionType); - } - } - break; + case ObjectCreationExpressionSyntax creation: + AddDependencyIfExists(creation.Type.ToString(), usings, dependencies); + break; - case ParameterSyntax parameter: - var paramType = GetFullTypeNameFromSymbol(parameter.Type?.ToString() ?? "", usings); - if (_solutionClasses.Contains(paramType)) - { - dependencies.Add(paramType); - } - break; + case InvocationExpressionSyntax invocation: + AnalyzeInvocation(invocation, usings, dependencies); + break; - case GenericNameSyntax generic: - foreach (var typeArg in generic.TypeArgumentList.Arguments) - { - var genericType = GetFullTypeNameFromSymbol(typeArg.ToString(), usings); - if (_solutionClasses.Contains(genericType)) - { - dependencies.Add(genericType); - } - } - break; - case IdentifierNameSyntax identifier: - var identifierType = GetFullTypeNameFromSymbol(identifier.Identifier.Text, usings); - if (_solutionClasses.Contains(identifierType)) - { - dependencies.Add(identifierType); - } - break; + case ParameterSyntax parameter: + AddDependencyIfExists(parameter.Type?.ToString() ?? "", usings, dependencies); + break; - case MemberAccessExpressionSyntax innerMemberAccess when !(innerMemberAccess.Parent is InvocationExpressionSyntax): - var memberType = GetFullTypeNameFromSymbol(innerMemberAccess.Expression.ToString(), usings); - if (_solutionClasses.Contains(memberType)) - { - dependencies.Add(memberType); - } - 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; } + } - return dependencies; + private static void AnalyzeInvocation(InvocationExpressionSyntax invocation, + List usings, HashSet dependencies) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + AddDependencyIfExists(memberAccess.Expression.ToString(), usings, dependencies); + } } - private static IEnumerable GetUsings(SyntaxNode node) + private static void AnalyzeGenericType(GenericNameSyntax generic, + List usings, HashSet dependencies) { - var root = node.SyntaxTree.GetRoot(); - var compilation = root as CompilationUnitSyntax; - return compilation?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); + 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) @@ -273,48 +268,22 @@ private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable(); + if (!typeName.Contains("<")) + return ResolveSimpleTypeName(typeName, usings); - 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 string.Join("", parts.Select(p => p.Contains("<") - ? p - : GetFullTypeNameFromSymbol(p, usings))); - } + return HandleGenericType(typeName, usings); + } - // Si le type contient déjà un namespace, on le retourne tel quel + private static string ResolveSimpleTypeName(string typeName, IEnumerable usings) + { if (typeName.Contains(".")) return typeName; - // Chercher dans les using si on trouve le namespace correspondant + return FindTypeInUsings(typeName, usings); + } + + private static string FindTypeInUsings(string typeName, IEnumerable usings) + { foreach (var usingStatement in usings) { var fullName = $"{usingStatement}.{typeName}"; @@ -323,11 +292,51 @@ private static string GetFullTypeNameFromSymbol(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(); From ac19c6b3324fa811de415641d86dd7b3d424eefc Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 20:02:45 +0100 Subject: [PATCH 05/13] feat: improve file cleanup and dependency analyzer reset --- CodeLineCounter.Tests/DataExporterTests.cs | 28 +++++++++++++++---- .../Services/CodeMetricsAnalyzer.cs | 1 + CodeLineCounter/Services/SolutionAnalyzer.cs | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index d206b88..e39eadf 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -216,9 +216,9 @@ 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", TargetClass = "ClassB", FilePath = "file1.cs", StartLine = 10 }, + }; var testFilePath = "test_export"; var format = CoreUtils.ExportFormat.JSON; @@ -233,8 +233,26 @@ public void export_dependencies_creates_files_in_correct_formats() Assert.True(File.Exists(expectedJsonPath)); Assert.True(File.Exists(expectedDotPath)); - File.Delete(expectedJsonPath); - File.Delete(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) diff --git a/CodeLineCounter/Services/CodeMetricsAnalyzer.cs b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs index 4c8187c..f8cddbe 100644 --- a/CodeLineCounter/Services/CodeMetricsAnalyzer.cs +++ b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs @@ -28,6 +28,7 @@ public static (List, Dictionary, int, int, List v).ToList(); var dependencyList = DependencyAnalyzer.GetDependencies(); + DependencyAnalyzer.Clear(); return (namespaceMetrics, projectTotals, totalLines, totalFilesAnalyzed, duplicationList, dependencyList); } diff --git a/CodeLineCounter/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs index b3d7390..950c309 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) { From c9fbdc5c4e599081f6ae518c8cacf61d5e0a2aed Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 20:06:45 +0100 Subject: [PATCH 06/13] refactor: simplify Export method by reusing ExportCollection --- CodeLineCounter/Utils/DataExporter.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index 2de7faf..412af07 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -15,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 From 6e4eebaa5c8be23f820520c1a000f2b6772e65f2 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 21:01:32 +0100 Subject: [PATCH 07/13] feat: add unit test for exporting results and enhance console output for dependencies --- .../SolutionAnalyzerTests.cs | 34 +++++++++++++++++++ CodeLineCounter/Services/SolutionAnalyzer.cs | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs index fc4d74c..b394cf6 100644 --- a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -114,5 +114,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/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs index 950c309..a85fb41 100644 --- a/CodeLineCounter/Services/SolutionAnalyzer.cs +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -15,7 +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) { @@ -97,6 +97,7 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor Console.WriteLine($"The data has been exported to {metricsOutputFilePath}"); Console.WriteLine($"The code duplications have been exported to {duplicationOutputFilePath}"); + Console.WriteLine($"The code depencencies have been exported to {dependenciesOutputFilePath} and the graph has been generated. (dot file can be found in the same directory)"); } catch (AggregateException ae) { From 183fddd5f8c18c678d7bd3f8116617d2faa5e5ac Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 21:07:32 +0100 Subject: [PATCH 08/13] docs: update README to include duplication report and dependency graph features --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1eb70d0..d4a533f 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,16 @@ 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.` 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 @@ -160,12 +163,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 +189,7 @@ CodeLineCounter/ │ ├── CyclomaticComplexityCalculatorTests.cs │ ├── CoreUtilsTests.cs │ ├── DataExporterTests.cs +│ ├── DependencyGraphGeneratorTests.cs │ ├── CsvHandlerTests.cs │ ├── FileUtilsTests.cs │ ├── HashUtilsTests.cs From 6fbf6e037f99475374347112b5ee250ba2a6e685 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 21:08:38 +0100 Subject: [PATCH 09/13] docs: fix version number in README for duplication report feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4a533f..bfd5a55 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ 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.` it also generate duplication report. +Since version `1.2.0` it also generate duplication report. All the results are exported to CSV or JSON files. A Graph of dependency is also generated. (dot file graphviz format) From efeefd83a25c51aeb56692266223bad5f3b81de8 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 21:10:09 +0100 Subject: [PATCH 10/13] fix: remove extra spaces in Dependencies.cs filename --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfd5a55..c1e13b1 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ CodeLineCounter/ ├── CodeLineCounter/ │ ├── Models/ │ │ └── AnalysisResult.cs -│ │ └── Dependencies .cs +│ │ └── Dependencies.cs │ │ └── DuplicationCode.cs │ │ └── NamespaceMetrics.cs │ ├── Services/ From e38b36fe61b034bd857d77b6f6f7abf426d5f9da Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Fri, 13 Dec 2024 21:13:28 +0100 Subject: [PATCH 11/13] test: add console output for constructed solution path in SolutionAnalyzerTests --- CodeLineCounter.Tests/SolutionAnalyzerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs index b394cf6..bdd665e 100644 --- a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -14,6 +14,8 @@ 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}"); From b05cc0c631a41129735c392de7a3b65ea6125a17 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Sat, 14 Dec 2024 11:42:36 +0100 Subject: [PATCH 12/13] refactor: update package references and improve code clarity in DependencyAnalyzer --- CodeLineCounter/CodeLineCounter.csproj | 5 +-- .../Services/DependencyAnalyzer.cs | 39 ++++++++++--------- CodeLineCounter/Services/SolutionAnalyzer.cs | 2 +- README.md | 6 +-- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index 6437bcb..5632fd9 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -13,12 +13,9 @@ all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + diff --git a/CodeLineCounter/Services/DependencyAnalyzer.cs b/CodeLineCounter/Services/DependencyAnalyzer.cs index 0cda23b..c54e4b7 100644 --- a/CodeLineCounter/Services/DependencyAnalyzer.cs +++ b/CodeLineCounter/Services/DependencyAnalyzer.cs @@ -13,7 +13,7 @@ namespace CodeLineCounter.Services { - public class DependencyAnalyzer + public static class DependencyAnalyzer { private static readonly ConcurrentDictionary> _dependencyMap = new(); private static readonly HashSet _solutionClasses = new(); @@ -57,7 +57,7 @@ private static void CollectAllClasses(IEnumerable projectFiles) private static string GetFullTypeName(ClassDeclarationSyntax classDeclaration) { - // Vérifier d'abord le namespace filescoped + // First check the file-scoped namespace var fileScopedNamespace = classDeclaration.SyntaxTree.GetRoot() .DescendantNodes() .OfType() @@ -67,7 +67,7 @@ private static string GetFullTypeName(ClassDeclarationSyntax classDeclaration) return $"{fileScopedNamespace.Name}.{classDeclaration.Identifier.Text}"; } - // Vérifier ensuite le namespace classique + // Then check the regular namespace var namespaceDeclaration = classDeclaration.Ancestors() .OfType() .FirstOrDefault(); @@ -103,8 +103,7 @@ public static void AnalyzeFile(string filePath, string sourceCode) { var tree = CSharpSyntaxTree.ParseText(sourceCode); var root = tree.GetRoot(); - var compilation = root as CompilationUnitSyntax; - var usings = compilation?.Usings.Select(u => u.Name.ToString()) ?? Enumerable.Empty(); + var classes = root.DescendantNodes().OfType(); @@ -129,7 +128,7 @@ public static void AnalyzeFile(string filePath, string sourceCode) { lock (_dependencyLock) { - // Vérifier si la relation existe déjà + // Check if the relation already exists if (!set.Any(r => r.Equals(relation))) { set.Add(relation); @@ -146,6 +145,7 @@ private static IEnumerable ExtractDependencies(ClassDeclarationSyntax cl { var dependencies = new HashSet(); var usings = GetUsingsWithCurrentNamespace(classDeclaration); + AnalyzeInheritance(classDeclaration, usings, dependencies); AnalyzeClassMembers(classDeclaration, usings, dependencies); @@ -153,12 +153,13 @@ private static IEnumerable ExtractDependencies(ClassDeclarationSyntax cl return dependencies; } - private static List GetUsingsWithCurrentNamespace(ClassDeclarationSyntax classDeclaration) + private static List GetUsingsWithCurrentNamespace(ClassDeclarationSyntax classDeclaration) { var root = classDeclaration.SyntaxTree.GetRoot(); var usings = (root as CompilationUnitSyntax)?.Usings - .Select(u => u.Name.ToString()) - .ToList() ?? new List(); + .Select(u => u.Name?.ToString()) + .Where(u => u != null) + .ToList() ?? new List(); var currentNamespace = classDeclaration.Ancestors() .OfType() @@ -173,7 +174,7 @@ private static List GetUsingsWithCurrentNamespace(ClassDeclarationSyntax } private static void AnalyzeInheritance(ClassDeclarationSyntax classDeclaration, - List usings, HashSet dependencies) + List usings, HashSet dependencies) { if (classDeclaration.BaseList == null) return; @@ -184,7 +185,7 @@ private static void AnalyzeInheritance(ClassDeclarationSyntax classDeclaration, } private static void AnalyzeClassMembers(ClassDeclarationSyntax classDeclaration, - List usings, HashSet dependencies) + List usings, HashSet dependencies) { var allNodes = classDeclaration.DescendantNodes(); @@ -194,7 +195,7 @@ private static void AnalyzeClassMembers(ClassDeclarationSyntax classDeclaration, } } - private static void AnalyzeNode(SyntaxNode node, List usings, HashSet dependencies) + private static void AnalyzeNode(SyntaxNode node, List usings, HashSet dependencies) { switch (node) { @@ -237,7 +238,7 @@ private static void AnalyzeNode(SyntaxNode node, List usings, HashSet usings, HashSet dependencies) + List usings, HashSet dependencies) { if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) { @@ -246,7 +247,7 @@ private static void AnalyzeInvocation(InvocationExpressionSyntax invocation, } private static void AnalyzeGenericType(GenericNameSyntax generic, - List usings, HashSet dependencies) + List usings, HashSet dependencies) { foreach (var typeArg in generic.TypeArgumentList.Arguments) { @@ -254,7 +255,7 @@ private static void AnalyzeGenericType(GenericNameSyntax generic, } } - private static void AddDependencyIfExists(string typeName, List usings, HashSet dependencies) + private static void AddDependencyIfExists(string typeName, List usings, HashSet dependencies) { var fullTypeName = GetFullTypeNameFromSymbol(typeName, usings); if (_solutionClasses.Contains(fullTypeName)) @@ -263,7 +264,7 @@ private static void AddDependencyIfExists(string typeName, List usings, } } - private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) + private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) { if (string.IsNullOrEmpty(typeName)) return typeName; @@ -274,7 +275,7 @@ private static string GetFullTypeNameFromSymbol(string typeName, IEnumerable usings) + private static string ResolveSimpleTypeName(string typeName, IEnumerable usings) { if (typeName.Contains(".")) return typeName; @@ -282,7 +283,7 @@ private static string ResolveSimpleTypeName(string typeName, IEnumerable return FindTypeInUsings(typeName, usings); } - private static string FindTypeInUsings(string typeName, IEnumerable usings) + private static string FindTypeInUsings(string typeName, IEnumerable usings) { foreach (var usingStatement in usings) { @@ -295,7 +296,7 @@ private static string FindTypeInUsings(string typeName, IEnumerable usin return typeName; } - private static string HandleGenericType(string typeName, IEnumerable usings) + private static string HandleGenericType(string typeName, IEnumerable usings) { var parts = SplitGenericType(typeName); return string.Join("", parts.Select(p => p.Contains("<") diff --git a/CodeLineCounter/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs index a85fb41..df3c579 100644 --- a/CodeLineCounter/Services/SolutionAnalyzer.cs +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -97,7 +97,7 @@ public static void ExportResults(AnalysisResult result, string solutionPath, Cor Console.WriteLine($"The data has been exported to {metricsOutputFilePath}"); Console.WriteLine($"The code duplications have been exported to {duplicationOutputFilePath}"); - Console.WriteLine($"The code depencencies have been exported to {dependenciesOutputFilePath} and the graph has been generated. (dot file can be found in the same directory)"); + 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/README.md b/README.md index c1e13b1..3603cba 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The `CodeLineCounter` project is a tool that counts the number of lines of code 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. A Graph of dependency is also generated. (dot file graphviz format) +All the results are exported to CSV or JSON files. A Graph of dependency is also generated. (dot file Graphviz format) ## Features @@ -131,7 +131,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 @@ -141,7 +141,7 @@ Code Hash,FilePath,MethodName,StartLine,NbLines { "CodeHash": "0133e750c0fec3d478670cb0441882855926c415a35aacf0360508fdeb73c34c", "FilePath": "C:\\temp\\NamespaceMetrics.cs", - "MethodName": "EtablirCommunication", + "MethodName": "OpenCommunication", "StartLine": 91, "NbLines": 3 }, From 0db91e72b0c208489d40dec5c95b81f1758eff83 Mon Sep 17 00:00:00 2001 From: Gildas Le Bournault Date: Sat, 14 Dec 2024 12:04:29 +0100 Subject: [PATCH 13/13] chore: update project file to suppress NU1701 warnings and improve README with additional NuGet package requirements --- CodeLineCounter/CodeLineCounter.csproj | 1 + CodeLineCounter/Models/DependencyRelation.cs | 2 +- README.md | 7 +++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CodeLineCounter/CodeLineCounter.csproj b/CodeLineCounter/CodeLineCounter.csproj index 5632fd9..c5455e2 100644 --- a/CodeLineCounter/CodeLineCounter.csproj +++ b/CodeLineCounter/CodeLineCounter.csproj @@ -5,6 +5,7 @@ net9.0 enable enable + NU1701 diff --git a/CodeLineCounter/Models/DependencyRelation.cs b/CodeLineCounter/Models/DependencyRelation.cs index ac1ca8c..284dafe 100644 --- a/CodeLineCounter/Models/DependencyRelation.cs +++ b/CodeLineCounter/Models/DependencyRelation.cs @@ -15,7 +15,7 @@ public class DependencyRelation [Name("StartLine")] public int StartLine { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is not DependencyRelation other) return false; diff --git a/README.md b/README.md index 3603cba..90ea9bd 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,16 @@ All the results are exported to CSV or JSON files. A Graph of dependency is also ## 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