Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
/CodeLineCounter.tests/coverageReport
/publish
/*.zip
/*.csv
/*.csv
/output.txt
2 changes: 1 addition & 1 deletion CodeLineCounter.Tests/CodeAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void TestAnalyzeSolution()
var analyzer = new CodeAnalyzer();

// Act
var (metrics, projectTotals, totalLines, totalFiles) = analyzer.AnalyzeSolution(solutionPath);
var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = analyzer.AnalyzeSolution(solutionPath);

// Assert
Assert.NotNull(metrics);
Expand Down
145 changes: 145 additions & 0 deletions CodeLineCounter.Tests/CodeDuplicationCheckerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CodeLineCounter.Services;
using Xunit;

namespace CodeLineCounter.Tests
{
public class CodeDuplicationCheckerTests
{
[Fact]
public void DetectCodeDuplicationInFiles_ShouldDetectDuplicates()
{
// Arrange
var file1 = "TestFile1.cs";
var file2 = "TestFile2.cs";

var code1 = @"
public class TestClass
{
public void TestMethod()
{
// Some code
var i = 1;
var j = 2;
var k = i + j;
}
}";

var code2 = @"
public class AnotherTestClass
{
public void AnotherTestMethod()
{
// Some code
var i = 1;
var j = 2;
var k = i + j;
}
}";

File.WriteAllText(file1, code1);
File.WriteAllText(file2, code2);

var files = new List<string> { file1, file2 };
var checker = new CodeDuplicationChecker();

// Act
checker.DetectCodeDuplicationInFiles(files);
var result = checker.GetCodeDuplicationMap();

// Assert
Assert.NotEmpty(result);
var duplicateEntry = result.First();
Assert.Equal(2, duplicateEntry.Value.Count); // Both files should be detected as duplicates

// Clean up
File.Delete(file1);
File.Delete(file2);
}

[Fact]
public void DetectCodeDuplicationInSourceCode_ShouldDetectDuplicates()
{
// Arrange
var checker = new CodeDuplicationChecker();

var sourceCode1 = @"
public class TestClass
{
public void TestMethod()
{
// Some code
var i = 1;
var j = 2;
var k = i + j;
}
}";

var sourceCode2 = @"
public class AnotherTestClass
{
public void AnotherTestMethod()
{
// Some code
var i = 1;
var j = 2;
var k = i + j;
}
}";

var file1 = "TestFile1.cs";
var file2 = "TestFile2.cs";

// Act
checker.DetectCodeDuplicationInSourceCode(file1, sourceCode1);
checker.DetectCodeDuplicationInSourceCode(file2, sourceCode2);
var result = checker.GetCodeDuplicationMap();

// Assert
Assert.NotEmpty(result);
var duplicateEntry = result.First();
Assert.Equal(2, duplicateEntry.Value.Count); // Both methods should be detected as duplicates
}

[Fact]
public void DetectCodeDuplicationInSourceCode_ShouldNotDetectDuplicatesForDifferentCode()
{
// Arrange
var checker = new CodeDuplicationChecker();

var sourceCode1 = @"
public class TestClass
{
public void TestMethod()
{
// Some code
var i = 1;
var j = 2 + i;
var k = j * j;
}
}";

var sourceCode2 = @"
public class AnotherTestClass
{
public void AnotherTestMethod()
{
// Different code
}
}";

var file1 = "TestFile1.cs";
var file2 = "TestFile2.cs";

// Act
checker.DetectCodeDuplicationInSourceCode(file1, sourceCode1);
checker.DetectCodeDuplicationInSourceCode(file2, sourceCode2);
var result = checker.GetCodeDuplicationMap();

// Assert
Assert.Empty(result); // No duplicates should be detected
}
}
}
50 changes: 50 additions & 0 deletions CodeLineCounter.Tests/CsvExporterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using CodeLineCounter.Models;
using CodeLineCounter.Utils;
using Xunit;

namespace CodeLineCounter.Tests
{
public class CsvExporterTests
{
[Fact]
public void GetFileDuplicationsCount_Should_Return_Correct_Count()
{

// Arrange
var file1 = "file1.cs";
var file2 = "file2.cs";
var file3 = "file3.cs";
var solutionPath = ".";

File.WriteAllText(file1, "");
File.WriteAllText(file2, "");
File.WriteAllText(file3, "");



// Arrange
var duplicationCounts = new Dictionary<string, int>
{
{ Path.GetFullPath(file1), 2 },
{ Path.GetFullPath(file2), 3 },
{ Path.GetFullPath(file3), 1 }
};

var metric = new NamespaceMetrics
{
FilePath = Path.GetFullPath("file2.cs")
};

// Act
int result = CsvExporter.GetFileDuplicationsCount(duplicationCounts, metric, solutionPath);

// Assert
Assert.Equal(3, result);

// Clean up
File.Delete(file1);
File.Delete(file2);
File.Delete(file3);
}
}
}
8 changes: 6 additions & 2 deletions CodeLineCounter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ private static void AnalyzeAndExportSolution(string solutionPath, bool verbose)
timer.Start();
string solutionFilename = Path.GetFileName(solutionPath);
string csvFilePath = $"{solutionFilename}-CodeMetrics.csv";
string duplicationCsvFilePath = $"{solutionFilename}-CodeDuplications.csv";

var analyzer = new CodeAnalyzer();
var (metrics, projectTotals, totalLines, totalFiles) = analyzer.AnalyzeSolution(solutionPath);
var (metrics, projectTotals, totalLines, totalFiles, duplicationMap) = analyzer.AnalyzeSolution(solutionPath);
timer.Stop();
TimeSpan timeTaken = timer.Elapsed;
string processingTime = $"Time taken: {timeTaken:m\\:ss\\.fff}";
Expand All @@ -103,9 +104,12 @@ private static void AnalyzeAndExportSolution(string solutionPath, bool verbose)
Console.WriteLine($"Processing completed, number of source files processed: {totalFiles}");
Console.WriteLine($"Total lines of code: {totalLines}");

CsvExporter.ExportToCsv(csvFilePath, metrics.ToList(), projectTotals, totalLines);
CsvExporter.ExportToCsv(csvFilePath, metrics.ToList(), projectTotals, totalLines, duplicationMap, solutionPath);
CsvExporter.ExportCodeDuplicationsToCsv(duplicationCsvFilePath, duplicationMap, solutionPath);
Console.WriteLine($"The data has been exported to {csvFilePath}");
Console.WriteLine($"The code duplications have been exported to {duplicationCsvFilePath}");
Console.WriteLine(processingTime);
}

}
}
12 changes: 9 additions & 3 deletions CodeLineCounter/Services/CodeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,28 @@ namespace CodeLineCounter.Services
{
public class CodeAnalyzer
{
public (List<NamespaceMetrics>, Dictionary<string, int>, int, int) AnalyzeSolution(string solutionFilePath)
public (List<NamespaceMetrics>, Dictionary<string, int>, int, int, Dictionary<string, List<(string filePath, string methodName, int startLine)>>) AnalyzeSolution(string solutionFilePath)
{
string solutionDirectory = Path.GetDirectoryName(solutionFilePath) ?? string.Empty;
var projectFiles = FileUtils.GetProjectFiles(solutionFilePath);

var namespaceMetrics = new List<NamespaceMetrics>();
var projectTotals = new Dictionary<string, int>();
var codeDuplicationChecker = new CodeDuplicationChecker();
int totalLines = 0;
int totalFilesAnalyzed = 0;

foreach (var projectFile in projectFiles)
{
AnalyzeProject(solutionDirectory, projectFile, ref totalFilesAnalyzed, ref totalLines, namespaceMetrics, projectTotals);

}

return (namespaceMetrics, projectTotals, totalLines, totalFilesAnalyzed);
codeDuplicationChecker.DetectCodeDuplicationInFiles(FileUtils.GetAllCsFiles(solutionDirectory));

var duplicationMap = codeDuplicationChecker.GetCodeDuplicationMap();

return (namespaceMetrics, projectTotals, totalLines, totalFilesAnalyzed, duplicationMap);
}

private void AnalyzeProject(string solutionDirectory, string projectFile, ref int totalFilesAnalyzed, ref int totalLines, List<NamespaceMetrics> namespaceMetrics, Dictionary<string, int> projectTotals)
Expand Down Expand Up @@ -122,4 +128,4 @@ public static void AnalyzeSourceCode(Dictionary<string, int> projectNamespaceMet
}
}
}
}
}
118 changes: 118 additions & 0 deletions CodeLineCounter/Services/CodeDuplicationChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using CodeLineCounter.Utils;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CodeLineCounter.Services
{
public class CodeDuplicationChecker
{
private readonly ConcurrentDictionary<string, HashSet<(string filePath, string methodName, int startLine)>> duplicationMap;
private readonly ConcurrentDictionary<string, HashSet<(string filePath, string methodName, int startLine)>> hashMap;
private readonly object duplicationLock = new object();

public CodeDuplicationChecker()
{
duplicationMap = new ConcurrentDictionary<string, HashSet<(string filePath, string methodName, int startLine)>>();
hashMap = new ConcurrentDictionary<string, HashSet<(string filePath, string methodName, int startLine)>>();
}

public void DetectCodeDuplicationInFiles(List<string> files)
{
Parallel.ForEach(files, file =>
{
var normalizedPath = Path.GetFullPath(file);
var sourceCode = File.ReadAllText(normalizedPath);
DetectCodeDuplicationInSourceCode(normalizedPath, sourceCode);
});

lock (duplicationLock)
{
foreach (var entry in hashMap)
{
if (entry.Value.Count > 1)
{
duplicationMap[entry.Key] = entry.Value;
}
}
}
}

public void DetectCodeDuplicationInSourceCode(string normalizedPath, string sourceCode)
{
var tree = CSharpSyntaxTree.ParseText(sourceCode);
var root = tree.GetRoot();
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();

Parallel.ForEach(methods, method =>
{
var blocks = ExtractBlocks(method);

foreach (var block in blocks)
{
var code = NormalizeCode(block.ToFullString());
var hash = HashUtils.ComputeHash(code);
var location = block.GetLocation().GetLineSpan().StartLinePosition.Line;

hashMap.AddOrUpdate(hash, new HashSet<(string filePath, string methodName, int startLine)>
{
(normalizedPath, method.Identifier.Text, location)
},
(key, set) =>
{
lock (set)
{
set.Add((normalizedPath, method.Identifier.Text, location));
}
return set;
});

// Debugging output
//Console.WriteLine($"Added block hash: {hash} from file: {normalizedPath}, method: {method.Identifier.Text}, line: {location}");
}
});

lock (duplicationLock)
{
foreach (var entry in hashMap)
{
if (entry.Value.Count > 1)
{
duplicationMap[entry.Key] = entry.Value;
}
}
}
}

public Dictionary<string, List<(string filePath, string methodName, int startLine)>> GetCodeDuplicationMap()
{
return duplicationMap.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToList());
}

private static IEnumerable<BlockSyntax> ExtractBlocks(MethodDeclarationSyntax method)
{
return method.DescendantNodes().OfType<BlockSyntax>();
}

private string NormalizeCode(string code)
{
var stringBuilder = new StringBuilder();
foreach (char c in code)
{
if (!char.IsWhiteSpace(c))
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString();
}
}
}
Loading