diff --git a/CodeLineCounter.Tests/CodeAnalyzerTests.cs b/CodeLineCounter.Tests/CodeAnalyzerTests.cs index 5fd4522..a40138c 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) = CodeAnalyzer.AnalyzeSolution(solutionPath); + var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = CodeMetricsAnalyzer.AnalyzeSolution(solutionPath); // Assert Assert.NotNull(metrics); @@ -37,7 +37,7 @@ public void AnalyzeSourceCode_Should_Set_CurrentNamespace() }; // Act - CodeAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out string? currentNamespace, out _, out _); + CodeMetricsAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out string? currentNamespace, out _, out _); // Assert Assert.Equal("MyNamespace", currentNamespace); @@ -57,7 +57,7 @@ public void AnalyzeSourceCode_Should_Set_FileLineCount() }; // Act - CodeAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out _, out int fileLineCount, out _); + CodeMetricsAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out _, out int fileLineCount, out _); // Assert - 3 lines only because comment lines are ignored Assert.Equal(3, fileLineCount); @@ -77,7 +77,7 @@ public void AnalyzeSourceCode_Should_Set_FileCyclomaticComplexity() }; // Act - CodeAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out _, out _, out int fileCyclomaticComplexity); + CodeMetricsAnalyzer.AnalyzeSourceCode(projectNamespaceMetrics, lines, out _, out _, out int fileCyclomaticComplexity); // Assert Assert.Equal(1, fileCyclomaticComplexity); diff --git a/CodeLineCounter.Tests/CodeDuplicationCheckerTests.cs b/CodeLineCounter.Tests/CodeDuplicationCheckerTests.cs index ace78a7..a7239f5 100644 --- a/CodeLineCounter.Tests/CodeDuplicationCheckerTests.cs +++ b/CodeLineCounter.Tests/CodeDuplicationCheckerTests.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using CodeLineCounter.Services; -using Xunit; namespace CodeLineCounter.Tests { diff --git a/CodeLineCounter.Tests/CoreUtilsTests.cs b/CodeLineCounter.Tests/CoreUtilsTests.cs index de1d79d..8cc0dea 100644 --- a/CodeLineCounter.Tests/CoreUtilsTests.cs +++ b/CodeLineCounter.Tests/CoreUtilsTests.cs @@ -59,6 +59,57 @@ public void ParseArguments_Should_Ignore_Invalid_Arguments() Assert.Equal("testDirectory", DirectoryPath); } + // ParseArguments correctly processes valid command line arguments with all options + [Fact] + public void ParseArguments_processes_valid_arguments_with_all_options() + { + // Arrange + string[] args = new[] { "-verbose", "-d", "C:/test", "-format", "JSON", "-help" }; + + // Act + var result = CoreUtils.ParseArguments(args); + + // Assert + Assert.True(result.Verbose); + Assert.Equal("C:/test", result.DirectoryPath); + Assert.True(result.Help); + Assert.Equal(CoreUtils.ExportFormat.JSON, result.format); + } + + // ParseArguments handles empty or null argument array + [Fact] + public void ParseArguments_handles_empty_argument_array() + { + // Arrange + string[] emptyArgs = Array.Empty(); + + // Act + var result = CoreUtils.ParseArguments(emptyArgs); + + // Assert + Assert.False(result.Verbose); + Assert.Null(result.DirectoryPath); + Assert.False(result.Help); + Assert.Equal(CoreUtils.ExportFormat.CSV, result.format); + } + + // ParseArguments processes invalid format option gracefully + [Fact] + public void ParseArguments_handles_invalid_format_option() + { + // Arrange + string[] args = new[] { "-format", "INVALID" }; + var consoleOutput = new StringWriter(); + Console.SetOut(consoleOutput); + + // Act + var result = CoreUtils.ParseArguments(args); + + // Assert + Assert.Equal(CoreUtils.ExportFormat.CSV, result.format); + Assert.Contains("Invalid format", consoleOutput.ToString()); + } + [Fact] public void GetUserChoice_Should_Return_Valid_Choice() { @@ -91,6 +142,39 @@ public void GetUserChoice_Should_Return_Invalid_Choice() Assert.Equal(-1, result); } + // GetUserChoice returns valid selection when input is within range + [Fact] + public void GetUserChoice_returns_valid_selection_for_valid_input() + { + // Arrange + var input = "2"; + var consoleInput = new StringReader(input); + Console.SetIn(consoleInput); + + // Act + int result = CoreUtils.GetUserChoice(3); + + // Assert + Assert.Equal(2, result); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("abc")] + public void GetUserChoice_handles_invalid_input(string input) + { + // Arrange + var consoleInput = new StringReader(input); + Console.SetIn(consoleInput); + + // Act + int result = CoreUtils.GetUserChoice(5); + + // Assert + Assert.Equal(-1, result); + } + [Fact] public void DisplaySolutions_Should_Write_Solutions_To_Console() { @@ -204,5 +288,21 @@ public void CheckSettings_WhenSettingsAreInvalid_ReturnsFalse() // Assert Assert.False(result); } + + [Theory] + [InlineData("CO.Solution.Build-CodeMetrics.txt", CoreUtils.ExportFormat.CSV)] + [InlineData("CO.Solution.Build-CodeDuplications.json", CoreUtils.ExportFormat.JSON)] + [InlineData("CO.Solution.Build-CodeMetrics.", CoreUtils.ExportFormat.CSV)] + [InlineData("CO.Solution.Build-CodeDuplications.²", CoreUtils.ExportFormat.JSON)] + [InlineData("metrics_789.csv", CoreUtils.ExportFormat.CSV)] + public void get_export_file_name_with_extension_handles_alphanumeric(string fileName, CoreUtils.ExportFormat format) + { + // Act + var result = CoreUtils.GetExportFileNameWithExtension(fileName, format); + + // Assert + Assert.Contains(Path.GetFileNameWithoutExtension(fileName), result); + Assert.True(File.Exists(result) || !File.Exists(result)); + } } } \ No newline at end of file diff --git a/CodeLineCounter.Tests/CsvHandlerTests.cs b/CodeLineCounter.Tests/CsvHandlerTests.cs index ba2dceb..2a01b51 100644 --- a/CodeLineCounter.Tests/CsvHandlerTests.cs +++ b/CodeLineCounter.Tests/CsvHandlerTests.cs @@ -1,9 +1,3 @@ -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using CsvHelper; -using System.Linq; -using Xunit; using CodeLineCounter.Utils; namespace CodeLineCounter.Tests diff --git a/CodeLineCounter.Tests/DataExporterTests.cs b/CodeLineCounter.Tests/DataExporterTests.cs index aa4ed06..7883b14 100644 --- a/CodeLineCounter.Tests/DataExporterTests.cs +++ b/CodeLineCounter.Tests/DataExporterTests.cs @@ -1,10 +1,5 @@ -using Xunit; -using System; -using System.IO; -using System.Collections.Generic; using CodeLineCounter.Models; using CodeLineCounter.Utils; -using System.Linq; namespace CodeLineCounter.Tests { diff --git a/CodeLineCounter.Tests/HashUtilsTest.cs b/CodeLineCounter.Tests/HashUtilsTests.cs similarity index 100% rename from CodeLineCounter.Tests/HashUtilsTest.cs rename to CodeLineCounter.Tests/HashUtilsTests.cs diff --git a/CodeLineCounter.Tests/SolutionAnalyzerTests.cs b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs new file mode 100644 index 0000000..3bf58e8 --- /dev/null +++ b/CodeLineCounter.Tests/SolutionAnalyzerTests.cs @@ -0,0 +1,113 @@ +using CodeLineCounter.Services; +using CodeLineCounter.Models; +using CodeLineCounter.Utils; + +namespace CodeLineCounter.Tests.Services +{ + public class SolutionAnalyzerTest + { + + [Fact] + public void PerformAnalysis_ShouldReturnCorrectAnalysisResult() + { + // Arrange + var basePath = FileUtils.GetBasePath(); + var solutionPath = Path.GetFullPath(Path.Combine(basePath, "..", "..", "..", "..")); + solutionPath = Path.Combine(solutionPath, "CodeLineCounter.sln"); + + // Act + var result = SolutionAnalyzer.PerformAnalysis(solutionPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("CodeLineCounter.sln", result.SolutionFileName); + } + + [Fact] + public void OutputAnalysisResults_ShouldPrintCorrectOutput() + { + // Arrange + var result = new SolutionAnalyzer.AnalysisResult + { + Metrics = new List(), + ProjectTotals = new Dictionary(), + TotalLines = 1000, + TotalFiles = 10, + DuplicationMap = new List(), + ProcessingTime = TimeSpan.FromSeconds(10), + SolutionFileName = "CodeLineCounter.sln", + DuplicatedLines = 100 + }; + var verbose = true; + + using (var sw = new StringWriter()) + { + Console.SetOut(sw); + + // Act + SolutionAnalyzer.OutputAnalysisResults(result, verbose); + + // Assert + var output = sw.ToString(); + Assert.Contains("Processing completed, number of source files processed: 10", output); + Assert.Contains("Total lines of code: 1000", output); + Assert.Contains("Solution CodeLineCounter.sln has 100 duplicated lines of code.", output); + Assert.Contains("Percentage of duplicated code: 10.00 %", output); + Assert.Contains("Time taken: 0:10.000", output); + } + } + + [Fact] + public void OutputDetailedMetrics_ShouldPrintMetricsAndProjectTotals() + { + // Arrange + var metrics = new List + { + new NamespaceMetrics + { + ProjectName = "Project1", + ProjectPath = "/path/to/project1", + NamespaceName = "Namespace1", + FileName = "File1.cs", + FilePath = "/path/to/project1/File1.cs", + LineCount = 100, + CyclomaticComplexity = 10 + }, + new NamespaceMetrics + { + ProjectName = "Project2", + ProjectPath = "/path/to/project2", + NamespaceName = "Namespace2", + FileName = "File2.cs", + FilePath = "/path/to/project2/File2.cs", + LineCount = 200, + CyclomaticComplexity = 20 + } + }; + + var projectTotals = new Dictionary + { + { "Project1", 100 }, + { "Project2", 200 } + }; + + using (var sw = new StringWriter()) + { + Console.SetOut(sw); + + // Act + SolutionAnalyzer.OutputDetailedMetrics(metrics, projectTotals); + + // Assert + var expectedOutput = + $"Project Project1 (/path/to/project1) - Namespace Namespace1 in file File1.cs (/path/to/project1/File1.cs) has 100 lines of code and a cyclomatic complexity of 10.{Environment.NewLine}" + + $"Project Project2 (/path/to/project2) - Namespace Namespace2 in file File2.cs (/path/to/project2/File2.cs) has 200 lines of code and a cyclomatic complexity of 20.{Environment.NewLine}" + + $"Project Project1 has 100 total lines of code.{Environment.NewLine}" + + $"Project Project2 has 200 total lines of code.{Environment.NewLine}"; + + Assert.Equal(expectedOutput, sw.ToString()); + } + } + + } +} \ No newline at end of file diff --git a/CodeLineCounter/Models/DuplicationCode.cs b/CodeLineCounter/Models/DuplicationCode.cs index 10b91f8..35c93e7 100644 --- a/CodeLineCounter/Models/DuplicationCode.cs +++ b/CodeLineCounter/Models/DuplicationCode.cs @@ -15,5 +15,27 @@ public class DuplicationCode [Name("NbLines")] public int NbLines { get; set; } } + + public class DuplicationInfo + { + [Name("Source File")] + public required string SourceFile { get; set; } + [Name("Start Line")] + public int StartLine { get; set; } + [Name("Nb Lines")] + public int NbLines { get; set; } + [Name("Duplicated Code")] + public required string DuplicatedCode { get; set; } + [Name("Duplicated In")] + public required List Duplicates { get; set; } + } + + public class DuplicationLocation + { + [Name("File Path")] + public required string FilePath { get; set; } + [Name("Start Line")] + public int StartLine { get; set; } + } } diff --git a/CodeLineCounter/Program.cs b/CodeLineCounter/Program.cs index 1a02e9e..014b779 100644 --- a/CodeLineCounter/Program.cs +++ b/CodeLineCounter/Program.cs @@ -1,12 +1,5 @@ using CodeLineCounter.Services; using CodeLineCounter.Utils; -using CodeLineCounter.Models; -using System.Diagnostics; -using System; -using System.IO; -using System.Linq; -using System.Collections.Generic; -using System.Collections.Specialized; namespace CodeLineCounter { @@ -15,71 +8,26 @@ static class Program static void Main(string[] args) { var settings = CoreUtils.ParseArguments(args); - if (CoreUtils.CheckSettings(settings) && settings.DirectoryPath != null) - { - // file deepcode ignore PT: Not a web server. This software is a console application. - var solutionFiles = FileUtils.GetSolutionFiles(settings.DirectoryPath); - if (solutionFiles.Count == 0) - { - Console.WriteLine("No solution (.sln) found in the specified directory."); - return; - } - - var solutionFilenameList = CoreUtils.GetFilenamesList(solutionFiles); - - CoreUtils.DisplaySolutions(solutionFilenameList); - int choice = CoreUtils.GetUserChoice(solutionFiles.Count); - if (choice == -1) return; - - var solutionPath = Path.GetFullPath(solutionFiles[choice - 1]); - AnalyzeAndExportSolution(solutionPath, settings.Verbose, settings.format); - } - } + if (!CoreUtils.CheckSettings(settings) || settings.DirectoryPath == null) + return; - private static void AnalyzeAndExportSolution(string solutionPath, bool verbose, CoreUtils.ExportFormat format ) - { - var timer = new Stopwatch(); - timer.Start(); - string solutionFilename = Path.GetFileName(solutionPath); - string csvFilePath = $"{solutionFilename}-CodeMetrics.csv"; - csvFilePath = CoreUtils.GetExportFileNameWithExtension(csvFilePath, format); - string duplicationCsvFilePath = $"{solutionFilename}-CodeDuplications.csv"; - duplicationCsvFilePath = CoreUtils.GetExportFileNameWithExtension(duplicationCsvFilePath, format); - - var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = CodeAnalyzer.AnalyzeSolution(solutionPath); - timer.Stop(); - TimeSpan timeTaken = timer.Elapsed; - string processingTime = $"Time taken: {timeTaken:m\\:ss\\.fff}"; - var nbLinesDuplicated = duplicationMap.Sum(x => x.NbLines); - var percentageDuplication = (nbLinesDuplicated / (double)totalLines) * 100; - - if (verbose) + // file deepcode ignore PT: Not a web server. This software is a console application. + var solutionFiles = FileUtils.GetSolutionFiles(settings.DirectoryPath); + if (solutionFiles.Count == 0) { - foreach (var metric in metrics) - { - Console.WriteLine($"Project {metric.ProjectName} ({metric.ProjectPath}) - Namespace {metric.NamespaceName} in file {metric.FileName} ({metric.FilePath}) has {metric.LineCount} lines of code and a cyclomatic complexity of {metric.CyclomaticComplexity}."); - } - - foreach (var projectTotal in projectTotals) - { - Console.WriteLine($"Project {projectTotal.Key} has {projectTotal.Value} total lines of code."); - } + Console.WriteLine("No solution (.sln) found in the specified directory."); + return; } - Console.WriteLine($"Processing completed, number of source files processed: {totalFiles}"); - Console.WriteLine($"Total lines of code: {totalLines}"); - Console.WriteLine($"Solution {solutionFilename} has {nbLinesDuplicated} duplicated lines of code."); - Console.WriteLine($"Percentage of duplicated code: {percentageDuplication:F2} %"); - Parallel.Invoke( - //() => CsvExporter.ExportToCsv(csvFilePath, metrics, projectTotals, totalLines, duplicationMap, solutionPath), - //() => CsvExporter.ExportCodeDuplicationsToCsv(duplicationCsvFilePath, duplicationMap), - () => DataExporter.ExportMetrics(csvFilePath, metrics, projectTotals, totalLines, duplicationMap, solutionPath, format), - () => DataExporter.ExportDuplications(duplicationCsvFilePath, duplicationMap, format) - ); + var solutionFilenameList = CoreUtils.GetFilenamesList(solutionFiles); + CoreUtils.DisplaySolutions(solutionFilenameList); + + var choice = CoreUtils.GetUserChoice(solutionFiles.Count); + if (choice == -1) + return; - Console.WriteLine($"The data has been exported to {csvFilePath}"); - Console.WriteLine($"The code duplications have been exported to {duplicationCsvFilePath}"); - Console.WriteLine(processingTime); + var solutionPath = Path.GetFullPath(solutionFiles[choice - 1]); + SolutionAnalyzer.AnalyzeAndExportSolution(solutionPath, settings.Verbose, settings.format); } } } \ No newline at end of file diff --git a/CodeLineCounter/Services/CodeAnalyzer.cs b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs similarity index 98% rename from CodeLineCounter/Services/CodeAnalyzer.cs rename to CodeLineCounter/Services/CodeMetricsAnalyzer.cs index af1d0c8..3c94664 100644 --- a/CodeLineCounter/Services/CodeAnalyzer.cs +++ b/CodeLineCounter/Services/CodeMetricsAnalyzer.cs @@ -1,14 +1,11 @@ using CodeLineCounter.Models; using CodeLineCounter.Utils; using Microsoft.CodeAnalysis.CSharp; -using System.IO; -using System.Collections.Generic; -using System.Linq; using Microsoft.CodeAnalysis; namespace CodeLineCounter.Services { - public static class CodeAnalyzer + public static class CodeMetricsAnalyzer { public static (List, Dictionary, int, int, List) AnalyzeSolution(string solutionFilePath) { diff --git a/CodeLineCounter/Services/SolutionAnalyzer.cs b/CodeLineCounter/Services/SolutionAnalyzer.cs new file mode 100644 index 0000000..8cd797b --- /dev/null +++ b/CodeLineCounter/Services/SolutionAnalyzer.cs @@ -0,0 +1,131 @@ +using CodeLineCounter.Utils; +using CodeLineCounter.Models; +using System.Diagnostics; +using System.Globalization; + +namespace CodeLineCounter.Services +{ + public static class SolutionAnalyzer + { + + public static void AnalyzeAndExportSolution(string solutionPath, bool verbose, CoreUtils.ExportFormat format) + { + try + { + var analysisResult = PerformAnalysis(solutionPath); + OutputAnalysisResults(analysisResult, verbose); + ExportResults(analysisResult, solutionPath, format); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error analyzing solution: {ex.Message}"); + throw; + } + } + + public static AnalysisResult PerformAnalysis(string solutionPath) + { + var timer = new Stopwatch(); + timer.Start(); + + var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = + CodeMetricsAnalyzer.AnalyzeSolution(solutionPath); + + timer.Stop(); + + return new AnalysisResult + { + Metrics = metrics, + ProjectTotals = projectTotals, + TotalLines = totalLines, + TotalFiles = totalFiles, + DuplicationMap = duplicationMap, + ProcessingTime = timer.Elapsed, + SolutionFileName = Path.GetFileName(solutionPath), + DuplicatedLines = duplicationMap.Sum(x => x.NbLines) + }; + } + + public static void OutputAnalysisResults(AnalysisResult result, bool verbose) + { + if (verbose) + { + OutputDetailedMetrics(result.Metrics, result.ProjectTotals); + } + + var percentageDuplication = (result.DuplicatedLines / (double)result.TotalLines) * 100; + NumberFormatInfo nfi = new System.Globalization.CultureInfo( "en-US", false ).NumberFormat; + + Console.WriteLine($"Processing completed, number of source files processed: {result.TotalFiles}"); + Console.WriteLine($"Total lines of code: {result.TotalLines}"); + Console.WriteLine($"Solution {result.SolutionFileName} has {result.DuplicatedLines} duplicated lines of code."); + Console.WriteLine($"Percentage of duplicated code: {percentageDuplication.ToString("F2", nfi)} %"); + Console.WriteLine($"Time taken: {result.ProcessingTime:m\\:ss\\.fff}"); + } + + public static void ExportResults(AnalysisResult result, string solutionPath, CoreUtils.ExportFormat format) + { + var metricsOutputFilePath = CoreUtils.GetExportFileNameWithExtension( + $"{result.SolutionFileName}-CodeMetrics.xxx", format); + var duplicationOutputFilePath = CoreUtils.GetExportFileNameWithExtension( + $"{result.SolutionFileName}-CodeDuplications.xxx", format); + + try + { + Parallel.Invoke( + () => DataExporter.ExportMetrics( + metricsOutputFilePath, + result.Metrics, + result.ProjectTotals, + result.TotalLines, + result.DuplicationMap, + solutionPath, + format), + () => DataExporter.ExportDuplications( + duplicationOutputFilePath, + result.DuplicationMap, + format) + ); + + Console.WriteLine($"The data has been exported to {metricsOutputFilePath}"); + Console.WriteLine($"The code duplications have been exported to {duplicationOutputFilePath}"); + } + catch (AggregateException ae) + { + Console.Error.WriteLine($"Error during parallel export operations: {ae.InnerException?.Message}"); + throw; + } + catch (IOException ioe) + { + Console.Error.WriteLine($"IO error during file operations: {ioe.Message}"); + throw; + } + } + + public sealed class AnalysisResult + { + public required List Metrics { get; set; } + public required Dictionary ProjectTotals { get; set; } + public int TotalLines { get; set; } + public int TotalFiles { get; set; } + public required List DuplicationMap { get; set; } + public TimeSpan ProcessingTime { get; set; } + public required string SolutionFileName { get; set; } + public int DuplicatedLines { get; set; } + } + + public static void OutputDetailedMetrics(List metrics, Dictionary projectTotals) + { + foreach (var metric in metrics) + { + Console.WriteLine($"Project {metric.ProjectName} ({metric.ProjectPath}) - Namespace {metric.NamespaceName} in file {metric.FileName} ({metric.FilePath}) has {metric.LineCount} lines of code and a cyclomatic complexity of {metric.CyclomaticComplexity}."); + } + + foreach (var projectTotal in projectTotals) + { + Console.WriteLine($"Project {projectTotal.Key} has {projectTotal.Value} total lines of code."); + } + } + + } +} \ No newline at end of file diff --git a/CodeLineCounter/Utils/CSVHandler.cs b/CodeLineCounter/Utils/CSVHandler.cs index a3f755d..365bbc4 100644 --- a/CodeLineCounter/Utils/CSVHandler.cs +++ b/CodeLineCounter/Utils/CSVHandler.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; using System.Globalization; -using System.IO; using CsvHelper; -using System.Linq; namespace CodeLineCounter.Utils { diff --git a/CodeLineCounter/Utils/CoreUtils.cs b/CodeLineCounter/Utils/CoreUtils.cs index 8de86b1..dcb5479 100644 --- a/CodeLineCounter/Utils/CoreUtils.cs +++ b/CodeLineCounter/Utils/CoreUtils.cs @@ -1,5 +1,4 @@ - namespace CodeLineCounter.Utils { public static class CoreUtils @@ -62,37 +61,55 @@ public static (bool Verbose, string? DirectoryPath, bool Help, ExportFormat form return (verbose, directoryPath, help, format); } + /// + /// Gets a valid user selection from the available solutions. + /// + /// The total number of available solutions + /// Selected solution number (1-based index) or -1 if invalid public static int GetUserChoice(int solutionCount) { - Console.Write("Choose a solution to analyze (enter the number): "); - if (int.TryParse(Console.ReadLine(), out int choice) && choice > 0 && choice <= solutionCount) + ArgumentOutOfRangeException.ThrowIfLessThan(solutionCount, 1); + + Console.Write($"Choose a solution to analyze (1-{solutionCount}): "); + + string? input = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(input)) { - return choice; + Console.WriteLine("No input provided. Please enter a valid number."); + return -1; } - else + + if (!int.TryParse(input, out int choice)) { - Console.WriteLine("Invalid selection. Please restart the program and choose a valid option."); + Console.WriteLine("Invalid input. Please enter a numeric value."); return -1; } - } - public static List GetFilenamesList(List solutionFiles) - { - List listOfFilenames = []; - for (int i = 0; i < solutionFiles.Count; i++) + if (choice < 1 || choice > solutionCount) { - if (File.Exists(solutionFiles[i])) - { - listOfFilenames.Add(Path.GetFileName(solutionFiles[i])); - } - else - { - listOfFilenames.Add(solutionFiles[i]); - } + Console.WriteLine($"Selection must be between 1 and {solutionCount}."); + return -1; } - return listOfFilenames; + return choice; + } + + /// + /// Processes a list of solution file paths and extracts their filenames. + /// + /// List of solution file paths to process + /// List of filenames or original paths for non-existing files + /// Thrown when solutionFiles is null + public static List GetFilenamesList(List solutionFiles) + { + ArgumentNullException.ThrowIfNull(solutionFiles); + return solutionFiles + .Select(file => File.Exists(file) + ? Path.GetFileName(file) + : file) + .ToList(); } public static void DisplaySolutions(List solutionFiles) @@ -123,17 +140,29 @@ public static bool CheckSettings((bool Verbose, string? DirectoryPath, bool Help public static string GetExportFileNameWithExtension(string filePath, CoreUtils.ExportFormat format) { - switch (format) + string newExtension = format switch + { + CoreUtils.ExportFormat.CSV => ".csv", + CoreUtils.ExportFormat.JSON => ".json", + _ => throw new ArgumentException($"Unsupported format: {format}", nameof(format)) + }; + + string currentExtension = Path.GetExtension(filePath); + + // If the file already has the desired extension (case-insensitive) + if (currentExtension.Equals(newExtension, StringComparison.OrdinalIgnoreCase)) + { + return filePath; + } + + // If the file has no extension, add the new one + if (string.IsNullOrEmpty(currentExtension)) { - case CoreUtils.ExportFormat.CSV: - filePath = Path.ChangeExtension(filePath, ".csv"); - break; - case CoreUtils.ExportFormat.JSON: - filePath = Path.ChangeExtension(filePath, ".json"); - break; + return filePath + newExtension; } - return filePath; + // If the file has a different extension, replace it + return Path.ChangeExtension(filePath, newExtension); } } diff --git a/CodeLineCounter/Utils/DataExporter.cs b/CodeLineCounter/Utils/DataExporter.cs index a0aeae7..e9eaa96 100644 --- a/CodeLineCounter/Utils/DataExporter.cs +++ b/CodeLineCounter/Utils/DataExporter.cs @@ -1,6 +1,3 @@ -using System.IO; -using System.Collections.Generic; -using CodeLineCounter.Utils; using CodeLineCounter.Models; namespace CodeLineCounter.Utils diff --git a/CodeLineCounter/Utils/FileUtils.cs b/CodeLineCounter/Utils/FileUtils.cs index aba7717..854d6a0 100644 --- a/CodeLineCounter/Utils/FileUtils.cs +++ b/CodeLineCounter/Utils/FileUtils.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; // Add this line +using System.Text.RegularExpressions; namespace CodeLineCounter.Utils { diff --git a/README.md b/README.md index b474449..4e75883 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,10 @@ NBLignesCount/ │ │ └── DuplicationCode.cs │ │ └── NamespaceMetrics.cs │ ├── Services/ -│ │ ├── CodeAnalyzer.cs +│ │ ├── CodeMetricsAnalyzer.cs │ │ ├── CodeDuplicationChecker.cs │ │ └── CyclomaticComplexityCalculator.cs +│ │ └── SolutionAnalyzer.cs │ ├── Utils/ │ │ ├── CoreUtils.cs │ │ │── CsvHandler.cs @@ -182,8 +183,9 @@ NBLignesCount/ │ ├── CoreUtilsTests.cs │ ├── DataExporterTests.cs │ ├── CsvHandlerTests.cs -│ ├── FileUtilsTest.cs -│ └── HashUtilsTest.cs +│ ├── FileUtilsTests.cs +│ ├── HashUtilsTests.cs +│ └── SolutionAnalyzerTests.cs ├── .gitignore ├── README.md ├── SECURITY.md