diff --git a/CC_JSON_SCHEMA_CHANGELOG.md b/CC_JSON_SCHEMA_CHANGELOG.md index 7c80bb9344..3a0f875ec5 100644 --- a/CC_JSON_SCHEMA_CHANGELOG.md +++ b/CC_JSON_SCHEMA_CHANGELOG.md @@ -92,3 +92,34 @@ export interface Fixed { } } ``` + +## 1.6 + +- An additional optional property `checksum` has been added to elements of `nodes` which are of type `File`. This checksum is calculated based on the files content and can be used by analyzers that update a `cc.json` to check if they need to recalculate the metrics of a node. +```json +{ + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "link": "", + "children": [ + { + "name": "Samplefile", + "type": "File", + "attributes": { + "complexity": 32.0, + "comment_lines": 46.0, + "rloc": 140.0, + "loc": 203.0 + }, + "link": "", + "children": [], + "checksum": "3ccac3cacd32f1c7" + } + ] + } + ] +} +``` diff --git a/analysis/CHANGELOG.md b/analysis/CHANGELOG.md index 51e1d140f3..769237cc6f 100644 --- a/analysis/CHANGELOG.md +++ b/analysis/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ## [unreleased] (Added πŸš€ | Changed | Removed | Fixed 🐞 | Chore πŸ‘¨β€πŸ’» πŸ‘©β€πŸ’») +### Added πŸš€ + +- Add new '--base-file' flag to unifiedparser and rawtextparser + ## [1.138.0] - 2025-10-08 ### Added πŸš€ diff --git a/analysis/analysers/AnalyserInterface/src/main/kotlin/de/maibornwolff/codecharta/analysers/analyserinterface/CommonAnalyserParameters.kt b/analysis/analysers/AnalyserInterface/src/main/kotlin/de/maibornwolff/codecharta/analysers/analyserinterface/CommonAnalyserParameters.kt index 48b9a6ee0c..dc815a9783 100644 --- a/analysis/analysers/AnalyserInterface/src/main/kotlin/de/maibornwolff/codecharta/analysers/analyserinterface/CommonAnalyserParameters.kt +++ b/analysis/analysers/AnalyserInterface/src/main/kotlin/de/maibornwolff/codecharta/analysers/analyserinterface/CommonAnalyserParameters.kt @@ -3,8 +3,13 @@ package de.maibornwolff.codecharta.analysers.analyserinterface import de.maibornwolff.codecharta.analysers.analyserinterface.util.CommaSeparatedParameterPreprocessor import de.maibornwolff.codecharta.analysers.analyserinterface.util.CommaSeparatedStringToListConverter import de.maibornwolff.codecharta.analysers.analyserinterface.util.FileExtensionConverter +import de.maibornwolff.codecharta.model.Node +import de.maibornwolff.codecharta.model.NodeType +import de.maibornwolff.codecharta.serialization.ProjectDeserializer +import de.maibornwolff.codecharta.util.Logger import picocli.CommandLine import java.io.File +import java.io.FileInputStream abstract class CommonAnalyserParameters { @CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["displays this help and exits"]) @@ -56,4 +61,51 @@ abstract class CommonAnalyserParameters { preprocessor = CommaSeparatedParameterPreprocessor::class ) protected var fileExtensionsToAnalyse: List = listOf() + + @CommandLine.Option( + names = ["-bf", "--base-file"], + description = ["base cc.json file with checksums to skip unchanged files during analysis"] + ) + protected var baseFile: File? = null + + protected fun loadBaseFileNodes(): Map { + if (baseFile == null) { + return emptyMap() + } + + if (!baseFile!!.exists()) { + Logger.warn { "Base file '${baseFile!!.absolutePath}' does not exist, continuing with normal analysis... " } + return emptyMap() + } + + return try { + val baseProject = ProjectDeserializer.deserializeProject(FileInputStream(baseFile!!)) + val nodeMap = mutableMapOf() + extractFileNodesWithPaths(baseProject.rootNode, "", nodeMap) + Logger.info { "Loaded ${nodeMap.size} file nodes from base file for checksum comparison" } + nodeMap + } catch (e: Exception) { + Logger.warn { "Failed to load base file: ${e.message}" } + emptyMap() + } + } + + private fun extractFileNodesWithPaths(node: Node, currentPath: String, nodeMap: MutableMap) { + // Skip the root node name to match the relative paths generated by parsers + val nodePath = if (currentPath.isEmpty() && node.name == "root") { + "" + } else if (currentPath.isEmpty()) { + node.name + } else { + "$currentPath/${node.name}" + } + + if (node.type == NodeType.File && node.checksum != null) { + nodeMap[nodePath] = node + } + + node.children.forEach { child -> + extractFileNodesWithPaths(child, nodePath, nodeMap) + } + } } diff --git a/analysis/analysers/filters/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/analysers/filters/mergefilter/ProjectMerger.kt b/analysis/analysers/filters/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/analysers/filters/mergefilter/ProjectMerger.kt index 6bc364dd24..70589105e5 100644 --- a/analysis/analysers/filters/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/analysers/filters/mergefilter/ProjectMerger.kt +++ b/analysis/analysers/filters/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/analysers/filters/mergefilter/ProjectMerger.kt @@ -126,16 +126,16 @@ class ProjectMerger( newAttributeDescriptor: AttributeDescriptor ) { if (existingAttributeDescriptor.title != newAttributeDescriptor.title) { - Logger.warn { "Title of '$metric' metric differs between files! Using value of first file..." } + Logger.info { "Title of '$metric' metric differs between files! Using value of first file..." } } if (existingAttributeDescriptor.description != newAttributeDescriptor.description) { - Logger.warn { "Description of '$metric' metric differs between files! Using value of first file..." } + Logger.info { "Description of '$metric' metric differs between files! Using value of first file..." } } if (existingAttributeDescriptor.link != newAttributeDescriptor.link) { - Logger.warn { "Link of '$metric' metric differs between files! Using value of first file..." } + Logger.info { "Link of '$metric' metric differs between files! Using value of first file..." } } if (existingAttributeDescriptor.direction != newAttributeDescriptor.direction) { - Logger.warn { "Direction of '$metric' metric differs between files! Using value of first file..." } + Logger.info { "Direction of '$metric' metric differs between files! Using value of first file..." } } } diff --git a/analysis/analysers/parsers/RawTextParser/README.md b/analysis/analysers/parsers/RawTextParser/README.md index 9284e540af..7eff64f0dc 100644 --- a/analysis/analysers/parsers/RawTextParser/README.md +++ b/analysis/analysers/parsers/RawTextParser/README.md @@ -15,23 +15,24 @@ This parser analyzes code, regardless of the programming language, to generate t ## Usage and Parameters -| Parameter | Description | -|-------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `FILE or FOLDER` | file/project to parseProject | -| `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | -| `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | -| `-fe, --file-extensions=` | comma-separated list of file-extensions to parse only those files (default: any) | -| `-h, --help` | displays this help and exits | -| `-m, --metrics=metrics` | comma-separated list of metrics to be computed (all available metrics are computed if not specified) | -| `--max-indentation-level=` | maximum Indentation Level (default 10) | -| `-nc, --not-compressed` | save uncompressed output File | -| `-o, --output-file=` | output File (or empty for stdout) | -| `--tab-width=` | tab width used (estimated if not provided) | -| `--verbose` | verbose mode | -| `--without-default-excludes` | (DEPRECATION WARNING: this flag will soon be disabled and replaced by '--include-build-folders') include build, target, dist, resources and out folders as well as files/folders starting with '.' | +| Parameter | Description | +|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FILE or FOLDER` | file/project to parseProject | +| `-bf, --base-file=` | base cc.json file with checksums to skip unchanged files during analysis | +| `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | +| `-fe, --file-extensions=` | comma-separated list of file-extensions to parse only those files (default: any) | +| `-h, --help` | displays this help and exits | +| `-m, --metrics=metrics` | comma-separated list of metrics to be computed (all available metrics are computed if not specified) | +| `--max-indentation-level=` | maximum Indentation Level (default 10) | +| `-nc, --not-compressed` | save uncompressed output File | +| `-o, --output-file=` | output File (or empty for stdout) | +| `--tab-width=` | tab width used (estimated if not provided) | +| `--verbose` | verbose mode | +| `--without-default-excludes` | ("DEPRECATION WARNING: this flag will soon be disabled and replaced by '--include-build-folders'") include build, target, dist, resources and out folders as well as files/folders starting with '.' | ``` Usage: ccsh rawtextparser [-h] [-nc] [--verbose] [--without-default-excludes] + [-bf=] [--max-indentation-level=] [-o=] [--tab-width=] [-e=]... [-fe=]... diff --git a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/FileMetrics.kt b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/FileMetrics.kt index 5b9c0415c7..c00a317b3c 100644 --- a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/FileMetrics.kt +++ b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/FileMetrics.kt @@ -2,6 +2,7 @@ package de.maibornwolff.codecharta.analysers.parsers.rawtext class FileMetrics { val metricsMap = mutableMapOf() + var checksum: String? = null fun addMetric(name: String, value: Number): FileMetrics { metricsMap[name] = value.toDouble() diff --git a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectGenerator.kt b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectGenerator.kt index ecd86bfae2..02a5a57fdb 100644 --- a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectGenerator.kt +++ b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectGenerator.kt @@ -23,7 +23,7 @@ class ProjectGenerator(private val projectBuilder: ProjectBuilder = ProjectBuild private fun addMetricsAsNodes(metricsMap: Map) { metricsMap.forEach { (key, fileMetrics) -> val (directory, fileName) = splitDirectoryAndFileName(key) - val node = MutableNode(fileName, attributes = fileMetrics.metricsMap) + val node = MutableNode(fileName, attributes = fileMetrics.metricsMap, checksum = fileMetrics.checksum) val path = PathFactory.fromFileSystemPath(directory) projectBuilder.insertByPath(path, node) } diff --git a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectMetricsCollector.kt b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectMetricsCollector.kt index b5b35f5ff0..bf63732464 100644 --- a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectMetricsCollector.kt +++ b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/ProjectMetricsCollector.kt @@ -3,12 +3,16 @@ package de.maibornwolff.codecharta.analysers.parsers.rawtext import de.maibornwolff.codecharta.analysers.parsers.rawtext.metrics.IndentationMetric import de.maibornwolff.codecharta.analysers.parsers.rawtext.metrics.LinesOfCodeMetric import de.maibornwolff.codecharta.analysers.parsers.rawtext.metrics.Metric +import de.maibornwolff.codecharta.model.ChecksumCalculator +import de.maibornwolff.codecharta.model.Node import de.maibornwolff.codecharta.progresstracker.ParsingUnit import de.maibornwolff.codecharta.progresstracker.ProgressTracker +import de.maibornwolff.codecharta.util.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.io.File +import java.util.concurrent.atomic.AtomicLong class ProjectMetricsCollector( private var root: File, @@ -17,10 +21,13 @@ class ProjectMetricsCollector( private val metricNames: List, private val verbose: Boolean, private val maxIndentLvl: Int, - private val tabWidth: Int + private val tabWidth: Int, + private val baseFileNodeMap: Map = emptyMap() ) { private var totalFiles = 0L private var filesParsed = 0L + private val filesSkipped = AtomicLong(0) + private val filesAnalyzed = AtomicLong(0) private val parsingUnit = ParsingUnit.Files private val progressTracker = ProgressTracker() private var excludePatterns = createExcludePatterns() @@ -44,7 +51,7 @@ class ProjectMetricsCollector( ) { filesParsed++ logProgress(it.name, filesParsed) - projectMetrics.addFileMetrics(standardizedPath, parseFile(it)) + projectMetrics.addFileMetrics(standardizedPath, collectMetricsForFile(it, standardizedPath)) lastFileName = it.name } } @@ -52,6 +59,7 @@ class ProjectMetricsCollector( } logProgress(lastFileName, totalFiles) + logStatistics() return projectMetrics } @@ -92,17 +100,41 @@ class ProjectMetricsCollector( return exclude.isNotEmpty() && excludePatterns.containsMatchIn(path) } - private fun parseFile(file: File): FileMetrics { + private fun collectMetricsForFile(file: File, standardizedPath: String): FileMetrics { + val fileContent = file.readText() + val currentChecksum = ChecksumCalculator.calculateChecksum(fileContent) + val baseNode = baseFileNodeMap[standardizedPath.removePrefix("/")] + return if (baseNode != null && baseNode.checksum == currentChecksum) { + reuseMetricsFromBaseFile(baseNode) + } else { + calculateMetricsForFile(file, currentChecksum) + } + } + + private fun reuseMetricsFromBaseFile(baseNode: Node): FileMetrics { + filesSkipped.incrementAndGet() + + val fileMetrics = FileMetrics() + baseNode.attributes.forEach { (key, value) -> + fileMetrics.addMetric(key, value.toString().toDoubleOrNull() ?: 0.0) + } + fileMetrics.checksum = baseNode.checksum + return fileMetrics + } + + private fun calculateMetricsForFile(file: File, checksum: String?): FileMetrics { + filesAnalyzed.incrementAndGet() + val metrics = createMetricsFromMetricNames() - file - .bufferedReader() - .useLines { lines -> lines.forEach { line -> metrics.forEach { it.parseLine(line) } } } + file.bufferedReader().useLines { lines -> lines.forEach { line -> metrics.forEach { it.parseLine(line) } } } if (metrics.isEmpty()) return FileMetrics() - return metrics.map { it.getValue() }.reduceRight { current: FileMetrics, acc: FileMetrics -> + val resultMetrics = metrics.map { it.getValue() }.reduceRight { current: FileMetrics, acc: FileMetrics -> acc.metricsMap.putAll(current.metricsMap) acc } + resultMetrics.checksum = checksum + return resultMetrics } private fun getStandardizedPath(file: File): String { @@ -120,4 +152,16 @@ class ProjectMetricsCollector( private fun logProgress(fileName: String, parsedFiles: Long) { progressTracker.updateProgress(totalFiles, parsedFiles, parsingUnit.name, fileName) } + + private fun logStatistics() { + if (baseFileNodeMap.isNotEmpty()) { + val skipped = filesSkipped.get() + val analyzed = filesAnalyzed.get() + val total = skipped + analyzed + Logger.info { + "Checksum comparison: $skipped files skipped, $analyzed files analyzed " + + "(${skipped * 100 / total.coerceAtLeast(1)}% reused)" + } + } + } } diff --git a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParser.kt b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParser.kt index 245f2b70de..5180aaebe2 100644 --- a/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParser.kt +++ b/analysis/analysers/parsers/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParser.kt @@ -2,9 +2,9 @@ package de.maibornwolff.codecharta.analysers.parsers.rawtext import de.maibornwolff.codecharta.analysers.analyserinterface.AnalyserDialogInterface import de.maibornwolff.codecharta.analysers.analyserinterface.AnalyserInterface +import de.maibornwolff.codecharta.analysers.analyserinterface.CommonAnalyserParameters import de.maibornwolff.codecharta.analysers.analyserinterface.util.CommaSeparatedParameterPreprocessor import de.maibornwolff.codecharta.analysers.analyserinterface.util.CommaSeparatedStringToListConverter -import de.maibornwolff.codecharta.analysers.analyserinterface.util.FileExtensionConverter import de.maibornwolff.codecharta.model.AttributeDescriptor import de.maibornwolff.codecharta.model.AttributeGenerator import de.maibornwolff.codecharta.serialization.ProjectDeserializer @@ -27,16 +27,7 @@ class RawTextParser( private val input: InputStream = System.`in`, private val output: PrintStream = System.out, private val error: PrintStream = System.err -) : AnalyserInterface, AttributeGenerator { - @CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["displays this help and exits"]) - private var help = false - - @CommandLine.Option(names = ["--verbose"], description = ["verbose mode"]) - private var verbose = false - - @CommandLine.Parameters(arity = "1", paramLabel = "FILE or FOLDER", description = ["file/project to parseProject"]) - private var inputFile: File? = null - +) : AnalyserInterface, AttributeGenerator, CommonAnalyserParameters() { @CommandLine.Option( names = ["-m", "--metrics"], description = [ @@ -49,34 +40,12 @@ class RawTextParser( ) private var metricNames: List = listOf() - @CommandLine.Option(names = ["-o", "--output-file"], description = ["output File (or empty for stdout)"]) - private var outputFile: String? = null - - @CommandLine.Option(names = ["-nc", "--not-compressed"], description = ["save uncompressed output File"]) - private var compress = true - @CommandLine.Option(names = ["--tab-width"], description = ["tab width used (estimated if not provided)"]) private var tabWidth: Int = DEFAULT_TAB_WIDTH @CommandLine.Option(names = ["--max-indentation-level"], description = ["maximum Indentation Level (default 10)"]) private var maxIndentLvl: Int = DEFAULT_INDENT_LVL - @CommandLine.Option( - names = ["-e", "--exclude"], - description = ["comma-separated list of regex patterns to exclude files/folders"], - converter = [(CommaSeparatedStringToListConverter::class)], - preprocessor = CommaSeparatedParameterPreprocessor::class - ) - private var exclude: List = listOf() - - @CommandLine.Option( - names = ["-fe", "--file-extensions"], - description = ["comma-separated list of file-extensions to parse only those files (default: any)"], - converter = [(FileExtensionConverter::class)], - preprocessor = CommaSeparatedParameterPreprocessor::class - ) - private var fileExtensions: List = listOf() - @CommandLine.Option( names = ["--without-default-excludes"], description = [ @@ -102,21 +71,24 @@ class RawTextParser( override fun call(): Unit? { logExecutionStartedSyncSignal() + val inputFile = inputFiles.firstOrNull() require(InputHelper.isInputValidAndNotNull(arrayOf(inputFile), canInputContainFolders = true)) { "Input invalid file for RawTextParser, stopping execution..." } - if (!withoutDefaultExcludes) exclude += DEFAULT_EXCLUDES + val excludePatterns = if (!withoutDefaultExcludes) patternsToExclude + DEFAULT_EXCLUDES else patternsToExclude + val baseFileNodeMap = loadBaseFileNodes() val projectMetrics: ProjectMetrics = ProjectMetricsCollector( inputFile!!, - exclude, - fileExtensions, + excludePatterns, + fileExtensionsToAnalyse, metricNames, verbose, maxIndentLvl, - tabWidth + tabWidth, + baseFileNodeMap ).parseProject() println() @@ -139,7 +111,7 @@ class RawTextParser( private fun logWarningsForNotFoundFileExtensions(projectMetrics: ProjectMetrics) { val notFoundFileExtensions = mutableListOf() - for (fileExtension in fileExtensions) { + for (fileExtension in fileExtensionsToAnalyse) { var isFileExtensionIncluded = false for (relativeFileName in projectMetrics.metricsMap.keys) { if (relativeFileName.contains(fileExtension)) { diff --git a/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/DialogTest.kt b/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/DialogTest.kt index e61e59fd5f..9d4d64137b 100644 --- a/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/DialogTest.kt +++ b/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/DialogTest.kt @@ -80,7 +80,7 @@ class DialogTest { terminal.press(Keys.ENTER) } - every { Dialog.Companion.testCallback() } returnsMany listOf( + every { Dialog.testCallback() } returnsMany listOf( fileCallback, outFileCallback, compressCallback, @@ -107,7 +107,7 @@ class DialogTest { assertThat(parseResult.matchedOption("without-default-excludes").getValue()).isEqualTo(withoutDefaultExcludes) assertThat(parseResult.matchedOption("verbose").getValue()).isEqualTo(verbose) assertThat(parseResult.matchedOption("exclude").getValue>()).isEqualTo(listOf(exclude)) - assertThat(parseResult.matchedPositional(0).getValue().name).isEqualTo(File(inputFileName).name) + assertThat(parseResult.matchedPositional(0).getValue>().first().name).isEqualTo(File(inputFileName).name) } @ParameterizedTest @@ -158,7 +158,7 @@ class DialogTest { terminal.press(Keys.ENTER) } - every { Dialog.Companion.testCallback() } returnsMany listOf( + every { Dialog.testCallback() } returnsMany listOf( inputFileCallback, outFileCallback, verboseCallback, @@ -183,7 +183,7 @@ class DialogTest { assertThat(parseResult.matchedOption("without-default-excludes").getValue()).isEqualTo(withoutDefaultExcludes) assertThat(parseResult.matchedOption("verbose").getValue()).isEqualTo(verbose) assertThat(parseResult.matchedOption("exclude").getValue>()).isEqualTo(listOf(exclude)) - assertThat(parseResult.matchedPositional(0).getValue().name).isEqualTo(File(inputFileName).name) + assertThat(parseResult.matchedPositional(0).getValue>().first().name).isEqualTo(File(inputFileName).name) } @Test @@ -239,7 +239,7 @@ class DialogTest { terminal.press(Keys.ENTER) } - every { Dialog.Companion.testCallback() } returnsMany listOf( + every { Dialog.testCallback() } returnsMany listOf( fileCallback, outFileCallback, compressCallback, @@ -257,7 +257,7 @@ class DialogTest { val commandLine = CommandLine(RawTextParser()) val parseResult = commandLine.parseArgs(*parserArguments.toTypedArray()) - assertThat(parseResult.matchedPositional(0).getValue().name).isEqualTo(File(inputFileName).name) + assertThat(parseResult.matchedPositional(0).getValue>().first().name).isEqualTo(File(inputFileName).name) } private fun provideInvalidTabWidth(): List { diff --git a/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParserTest.kt b/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParserTest.kt index 85f0869bf1..e9564c8053 100644 --- a/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParserTest.kt +++ b/analysis/analysers/parsers/RawTextParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/rawtext/RawTextParserTest.kt @@ -293,4 +293,44 @@ class RawTextParserTest { // then Assertions.assertThat(lambdaSlot.any { e -> e().contains(invalidMetricName) }).isTrue() } + + @Test + fun `should reuse metrics from base file when checksums match`() { + // given + val pipedProject = "" + val inputFilePath = "src/test/resources/sampleproject" + val baseFilePath = "src/test/resources/cc_projects/project_5.cc.json" + val expectedResultFile = File("src/test/resources/cc_projects/project_5.cc.json").absoluteFile + System.setErr(PrintStream(errContent)) + + // when + val result = executeForOutput(pipedProject, arrayOf(inputFilePath, "--base-file=$baseFilePath")) + + // then + Assertions.assertThat(errContent.toString()).contains("Loaded 5 file nodes from base file for checksum comparison") + Assertions.assertThat(errContent.toString()).contains("Checksum comparison: 5 files skipped, 0 files analyzed (100% reused)") + JSONAssert.assertEquals(result, expectedResultFile.readText(), JSONCompareMode.NON_EXTENSIBLE) + + // clean up + System.setErr(originalErr) + } + + @Test + fun `should show warning when base file does not exist`() { + // given + val pipedProject = "" + val inputFilePath = "src/test/resources/sampleproject" + val baseFilePath = "src/test/resources/cc_projects/nonexistent.cc.json" + System.setErr(PrintStream(errContent)) + + // when + executeForOutput(pipedProject, arrayOf(inputFilePath, "--base-file=$baseFilePath")) + + // then + Assertions.assertThat(errContent.toString()).contains("Base file") + Assertions.assertThat(errContent.toString()).contains("does not exist, continuing with normal analysis...") + + // clean up + System.setErr(originalErr) + } } diff --git a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_3.cc.json b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_3.cc.json index 2f3c55717c..009810391a 100644 --- a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_3.cc.json +++ b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_3.cc.json @@ -1,4 +1,5 @@ { + "checksum": "7c0d9d9e3cd987aa7b1e78b078b87583", "data": { "projectName": "", "nodes": [ @@ -26,7 +27,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "fa749fc4025fc353" } ] } @@ -169,6 +171,5 @@ } }, "blacklist": [] - }, - "checksum": "9bc26c769e60d5c80877279cbec5cc91" + } } diff --git a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_4.cc.json b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_4.cc.json index 8c9e31a2dd..509ae69618 100644 --- a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_4.cc.json +++ b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_4.cc.json @@ -1,4 +1,5 @@ { + "checksum": "5185776ce4e86e238c8fec2ff3acff13", "data": { "projectName": "", "nodes": [ @@ -24,7 +25,8 @@ "indentation_level_2+": 3.0 }, "link": "", - "children": [] + "children": [], + "checksum": "a7714a3a621e7270" }, { "name": "spaces_4.included", @@ -36,7 +38,8 @@ "indentation_level_2+": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "9134b1ebb1e5ba80" }, { "name": "spaces_5.includedtoo", @@ -48,7 +51,8 @@ "indentation_level_2+": 3.0 }, "link": "", - "children": [] + "children": [], + "checksum": "73d843464a419a54" }, { "name": "spaces_x_not_included.excluded", @@ -60,7 +64,8 @@ "indentation_level_2+": 3.0 }, "link": "", - "children": [] + "children": [], + "checksum": "58927614af51eb27" } ] } @@ -117,6 +122,5 @@ } }, "blacklist": [] - }, - "checksum": "7a4f4b06b14eb4565a0fe2985d66fccf" + } } diff --git a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_5.cc.json b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_5.cc.json index 07fa157f68..74f92a1d5a 100644 --- a/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_5.cc.json +++ b/analysis/analysers/parsers/RawTextParser/src/test/resources/cc_projects/project_5.cc.json @@ -1,4 +1,5 @@ { + "checksum": "da01a120bc63624ebe23d2c4ea4aec34", "data": { "projectName": "", "nodes": [ @@ -32,7 +33,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "a7714a3a621e7270" }, { "name": "spaces_4.included", @@ -52,7 +54,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "9134b1ebb1e5ba80" }, { "name": "spaces_5.includedtoo", @@ -72,7 +75,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "73d843464a419a54" }, { "name": "spaces_x_not_included.excluded", @@ -92,7 +96,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "58927614af51eb27" } ] }, @@ -114,7 +119,8 @@ "indentation_level_10+": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "fa749fc4025fc353" } ] } @@ -257,6 +263,5 @@ } }, "blacklist": [] - }, - "checksum": "d55e95bd9b4c45b226abcd83290e9700" + } } diff --git a/analysis/analysers/parsers/UnifiedParser/README.md b/analysis/analysers/parsers/UnifiedParser/README.md index bb033362cc..4e86653241 100644 --- a/analysis/analysers/parsers/UnifiedParser/README.md +++ b/analysis/analysers/parsers/UnifiedParser/README.md @@ -45,6 +45,7 @@ Some metrics are calculated on a per-function basis rather than per-file. Each o | Parameter | Description | |-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| | `FOLDER or FILE` | The project folder or code file to parse. To merge the result with an existing project piped into STDIN, pass a '-' as an additional argument | +| `-bf, --base-file=` | base cc.json file with checksums to skip unchanged files during analysis | | `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | | `-fe, --file-extensions=` | comma-separated list of file-extensions to parse only those files (default: any) | | `-h, --help` | displays this help and exits | @@ -54,9 +55,9 @@ Some metrics are calculated on a per-function basis rather than per-file. Each o | `--verbose` | displays messages about parsed and ignored files | ``` -Usage: ccsh unifiedparser [-h] [-ibf] [-nc] [--verbose] [-o=] - [-e=]... - [-fe=]... FILE or FOLDER... +Usage: ccsh unifiedparser [-h] [-ibf] [-nc] [--verbose] [-bf=] + [-o=] [-e=]... + [-fe=]... FILE or FOLDER.. ``` ## Examples diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/ProjectScanner.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/ProjectScanner.kt index 8a5751747d..7527bce1d4 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/ProjectScanner.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/ProjectScanner.kt @@ -1,7 +1,9 @@ package de.maibornwolff.codecharta.analysers.parsers.unified import de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors.AvailableCollectors +import de.maibornwolff.codecharta.model.ChecksumCalculator import de.maibornwolff.codecharta.model.MutableNode +import de.maibornwolff.codecharta.model.Node import de.maibornwolff.codecharta.model.PathFactory import de.maibornwolff.codecharta.model.ProjectBuilder import de.maibornwolff.codecharta.progresstracker.ParsingUnit @@ -13,17 +15,21 @@ import kotlinx.coroutines.runBlocking import java.io.File import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong class ProjectScanner( private val root: File, private val projectBuilder: ProjectBuilder, private val excludePatterns: List = listOf(), - private val includeExtensions: List = listOf() + private val includeExtensions: List = listOf(), + private val baseFileNodes: Map = emptyMap() ) { private var totalFiles = 0L private var filesParsed = 0L private var ignoredFiles = 0L private val ignoredFileTypes = mutableSetOf() + private val filesSkipped = AtomicLong(0) + private val filesAnalyzed = AtomicLong(0) private val parsingUnit = ParsingUnit.Files private val progressTracker = ProgressTracker() @@ -56,7 +62,7 @@ class ProjectScanner( parsableFiles.forEach { file -> launch { filesParsed++ - applyLanguageSpecificCollector(file, verbose) + collectMetricsForFile(file, verbose) if (!verbose) logProgress(file.name, filesParsed) } } @@ -64,22 +70,50 @@ class ProjectScanner( progressTracker.updateProgress(totalFiles, totalFiles, parsingUnit.name) System.err.println() + + if (baseFileNodes.isNotEmpty()) { + logBaseFileStatistics() + } + if (verbose) { Logger.info { "Analysis of files complete, creating output file..." } } addAllNodesToProjectBuilder() } - private fun applyLanguageSpecificCollector(file: File, verbose: Boolean) { + private fun collectMetricsForFile(file: File, verbose: Boolean) { val relativeFilePath = getRelativeFileName(file.toString()) require(file.isFile) { "Expected file but found folder at $relativeFilePath!" } - val collector = findCollectorForFileType(file.extension)?.collectorFactory + val fileContent = file.readText() + val currentChecksum = ChecksumCalculator.calculateChecksum(fileContent) + val baseNode = baseFileNodes[relativeFilePath] + if (baseNode != null && baseNode.checksum == currentChecksum) { + reuseMetricsFromBaseFile(baseNode, relativeFilePath, verbose) + } else { + applyLanguageSpecificCollector(file, fileContent, relativeFilePath, verbose) + } + } - require(collector != null) { "Unexpectedly encountered an unsupported file at $relativeFilePath!" } + private fun reuseMetricsFromBaseFile(baseNode: Node, relativeFilePath: String, verbose: Boolean) { + if (verbose) Logger.info { "Reusing metrics for unchanged file $relativeFilePath" } + fileMetrics[relativeFilePath] = MutableNode( + name = baseNode.name, + type = baseNode.type, + attributes = baseNode.attributes, + checksum = baseNode.checksum + ) + filesSkipped.incrementAndGet() + } + private fun applyLanguageSpecificCollector(file: File, fileContent: String, relativeFilePath: String, verbose: Boolean) { if (verbose) Logger.info { "Calculating metrics for file $relativeFilePath" } - fileMetrics[relativeFilePath] = collector().collectMetricsForFile(file) + + val collector = findCollectorForFileType(file.extension)?.collectorFactory + require(collector != null) { "Unexpectedly encountered an unsupported file at $relativeFilePath!" } + + fileMetrics[relativeFilePath] = collector().collectMetricsForFile(file, fileContent) + filesAnalyzed.incrementAndGet() } private fun getRelativeFileName(fileName: String): String { @@ -120,6 +154,16 @@ class ProjectScanner( progressTracker.updateProgress(totalFiles, parsedFiles, parsingUnit.name, fileName) } + private fun logBaseFileStatistics() { + val skipped = filesSkipped.get() + val analyzed = filesAnalyzed.get() + val total = skipped + analyzed + Logger.info { + "Checksum comparison: $skipped files skipped, $analyzed files analyzed " + + "(${skipped * 100 / total.coerceAtLeast(1)}% reused)" + } + } + private fun addAllNodesToProjectBuilder() { val sortedFileMetrics = fileMetrics.entries.sortedBy { (relativePath, _) -> relativePath } for ((relativePath, node) in sortedFileMetrics) { diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParser.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParser.kt index 7f8c4418b6..1458d63ef3 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParser.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParser.kt @@ -69,7 +69,8 @@ class UnifiedParser( private fun scanInputProject(inputFile: File): Project { val startTime = System.currentTimeMillis() val projectBuilder = ProjectBuilder() - val projectScanner = ProjectScanner(inputFile, projectBuilder, patternsToExclude, fileExtensionsToAnalyse) + val baseFileNodeMap = loadBaseFileNodes() + val projectScanner = ProjectScanner(inputFile, projectBuilder, patternsToExclude, fileExtensionsToAnalyse, baseFileNodeMap) projectScanner.traverseInputProject(verbose) val notFoundButSpecifiedFormats = projectScanner.getNotFoundFileExtensions() @@ -96,7 +97,7 @@ class UnifiedParser( val executionTimeMs = System.currentTimeMillis() - startTime val formattedTime = formatTime(executionTimeMs.milliseconds) - System.err.println("UnifiedParser completed in $formattedTime, building project...") + System.err.println("\nUnifiedParser completed in $formattedTime, building project...") projectBuilder.addAttributeDescriptions(getAttributeDescriptors()) diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt index 25c384e28f..9deda0700f 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/MetricCollector.kt @@ -4,6 +4,7 @@ import de.maibornwolff.codecharta.analysers.parsers.unified.metriccalculators.Ca import de.maibornwolff.codecharta.analysers.parsers.unified.metriccalculators.MetricsToCalculatorsMap import de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes.AvailableFileMetrics import de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes.MetricNodeTypes +import de.maibornwolff.codecharta.model.ChecksumCalculator import de.maibornwolff.codecharta.model.MutableNode import de.maibornwolff.codecharta.model.NodeType import org.treesitter.TSLanguage @@ -23,7 +24,13 @@ abstract class MetricCollector( private val metricPerFileInfo = metricCalculators.getPerFileMetricInfo() fun collectMetricsForFile(file: File): MutableNode { - val rootNode = getRootNode(file) + val fileContent = file.readText() + return collectMetricsForFile(file, fileContent) + } + + fun collectMetricsForFile(file: File, fileContent: String): MutableNode { + val checksum = ChecksumCalculator.calculateChecksum(fileContent) + val rootNode = getRootNode(fileContent) rootNodeType = rootNode.type // we use an IntArray and not a map here as it improves performance @@ -45,14 +52,15 @@ abstract class MetricCollector( return MutableNode( name = file.name, type = NodeType.File, - attributes = metricNameToValue + attributes = metricNameToValue, + checksum = checksum ) } - private fun getRootNode(file: File): TSNode { + private fun getRootNode(fileContent: String): TSNode { val parser = TSParser() parser.language = treeSitterLanguage - val rootNode = parser.parseString(null, file.readText()).rootNode + val rootNode = parser.parseString(null, fileContent).rootNode return rootNode } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt index 10ef463e59..17feafcc1d 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt @@ -270,4 +270,44 @@ class UnifiedParserTest { // then JSONAssert.assertEquals(result, expectedResultFile.readText(), JSONCompareMode.NON_EXTENSIBLE) } + + @Test + fun `should reuse metrics from base file when checksums match`() { + // given + val pipedProject = "" + val inputFilePath = "${testResourceBaseFolder}sampleproject" + val baseFilePath = "${testResourceBaseFolder}sampleProject.cc.json" + val expectedResultFile = File("${testResourceBaseFolder}sampleProject.cc.json").absoluteFile + System.setErr(PrintStream(errContent)) + + // when + val result = executeForOutput(pipedProject, arrayOf(inputFilePath, "--base-file=$baseFilePath")) + + // then + Assertions.assertThat(errContent.toString()).contains("Loaded 6 file nodes from base file for checksum comparison") + Assertions.assertThat(errContent.toString()).contains("Checksum comparison: 6 files skipped, 0 files analyzed (100% reused)") + JSONAssert.assertEquals(result, expectedResultFile.readText(), JSONCompareMode.NON_EXTENSIBLE) + + // clean up + System.setErr(originalErr) + } + + @Test + fun `should show warning when base file does not exist`() { + // given + val pipedProject = "" + val inputFilePath = "${testResourceBaseFolder}sampleproject" + val baseFilePath = "${testResourceBaseFolder}nonexistent.cc.json" + System.setErr(PrintStream(errContent)) + + // when + executeForOutput(pipedProject, arrayOf(inputFilePath, "--base-file=$baseFilePath")) + + // then + Assertions.assertThat(errContent.toString()).contains("Base file") + Assertions.assertThat(errContent.toString()).contains("does not exist, continuing with normal analysis...") + + // clean up + System.setErr(originalErr) + } } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/excludePattern.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/excludePattern.cc.json index b1580a8c43..ed4f72a93b 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/excludePattern.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/excludePattern.cc.json @@ -1,5 +1,5 @@ { - "checksum": "a39e9dd1902db5f632510b548e907349", + "checksum": "6a6a1622a79766fd14945bcfa7bf0edd", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 1.0 }, "link": "", - "children": [] + "children": [], + "checksum": "e31a24ef7db12378" }, { "name": "whenCase.kt", @@ -59,7 +60,8 @@ "median_rloc_per_function": 6.0 }, "link": "", - "children": [] + "children": [], + "checksum": "91308e658fb97d1c" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json index 57dc59898e..d5fae49354 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/includeAll.cc.json @@ -1,5 +1,5 @@ { - "checksum": "ca6213ee87881967e3d17c115900618b", + "checksum": "7603bc8970dcad43c1a0931d30cff7d1", "data": { "projectName": "", "nodes": [ @@ -39,7 +39,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "45563eb7b21bf6cc" } ] }, @@ -73,7 +74,8 @@ "median_rloc_per_function": 4.5 }, "link": "", - "children": [] + "children": [], + "checksum": "c40238c79d87defc" }, { "name": "hello.kt", @@ -99,7 +101,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "bfc329a4e973c424" } ] }, @@ -127,7 +130,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "39e5446f9451898e" }, { "name": "foo.py", @@ -153,7 +157,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "45b3835ff9d57197" }, { "name": "helloWorld.ts", @@ -179,7 +184,8 @@ "median_rloc_per_function": 1.0 }, "link": "", - "children": [] + "children": [], + "checksum": "e31a24ef7db12378" }, { "name": "whenCase.kt", @@ -205,7 +211,8 @@ "median_rloc_per_function": 6.0 }, "link": "", - "children": [] + "children": [], + "checksum": "91308e658fb97d1c" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinOnly.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinOnly.cc.json index fa9c824ac2..6f5b534cc9 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinOnly.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/kotlinOnly.cc.json @@ -1,5 +1,5 @@ { - "checksum": "66811a387365ec243d7dc8782b97ec34", + "checksum": "714d53b66c6eed19492f96555408e2d5", "data": { "projectName": "", "nodes": [ @@ -39,7 +39,8 @@ "median_rloc_per_function": 4.5 }, "link": "", - "children": [] + "children": [], + "checksum": "c40238c79d87defc" }, { "name": "hello.kt", @@ -65,7 +66,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "bfc329a4e973c424" } ] }, @@ -93,7 +95,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "39e5446f9451898e" }, { "name": "whenCase.kt", @@ -119,7 +122,8 @@ "median_rloc_per_function": 6.0 }, "link": "", - "children": [] + "children": [], + "checksum": "91308e658fb97d1c" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/bashSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/bashSample.cc.json index a02a3be81e..363105c7a1 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/bashSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/bashSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "11f88f869e67d13c67e139fe3201bfcc", + "checksum": "a02895f67353c103a8d95687d0c90ecb", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 12.5 }, "link": "", - "children": [] + "children": [], + "checksum": "3ccac3cacd32f1c7" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cHeaderSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cHeaderSample.cc.json index ab4af84576..906c229231 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cHeaderSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cHeaderSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "c4aa56c96eba7b5e7238705dc0d714fe", + "checksum": "adb80b857fc42f8d54bdc41c7730577a", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "5e677c486ac21c7c" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSample.cc.json index 03367a18f9..d293572978 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "e169294deb6b26fb4953961d5df94063", + "checksum": "a28334ba54edb523f67d06134eada5d7", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "9040b34edc042441" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json index c9867592ff..359ca9bd00 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cSharpSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "21708b46ca40090386c5bbd2dcc5a30f", + "checksum": "4ecee3af3aca716763d85fb81d757925", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 2.0 }, "link": "", - "children": [] + "children": [], + "checksum": "b75bdf7e2fa9ac20" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppHeaderSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppHeaderSample.cc.json index 318e2eb28f..55d0f00df1 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppHeaderSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppHeaderSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "06e3617bcd47ae806a34fc59d0dd2556", + "checksum": "82b17826b0ffd2de55ff39313e092ef5", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "bcf4c4c605556896" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppSample.cc.json index 23fc3c95b3..41dd8c1a86 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/cppSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "209ec4b418646125cbc8cf07e00b4fe5", + "checksum": "734dd57b1638e6d1e274b87d0126f57f", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "5bfb2bd13293754b" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/goSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/goSample.cc.json index da20a7c589..247ffdf06d 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/goSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/goSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "47d9ae4563ad257fab0886b83ca22cf9", + "checksum": "13c12e696c950d313d445b8007248871", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 3.0 }, "link": "", - "children": [] + "children": [], + "checksum": "a01ebf0cb7aaab6b" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json index 3ebc594f72..0251d992b7 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javaSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "c0a7e9e0a4d690f701ea3496d6f2e6e1", + "checksum": "82c93a38d243a332e42f45259d0394ae", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "c017327e52f10868" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json index f1c5301f3a..8bdc1e6a2e 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/javascriptSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "864bd81dcd041eb86585d9514970d18d", + "checksum": "987ed34135ccc1c53db815745bd099f4", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 2.0 }, "link": "", - "children": [] + "children": [], + "checksum": "fba54a2db29ac35e" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json index e0c8d038a7..fbeb5c3112 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/kotlinSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "e8253a3f6506e97982b13bb44ced3319", + "checksum": "6dbd44632ba3bff3881baf16c14768b8", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 2.0 }, "link": "", - "children": [] + "children": [], + "checksum": "8312d271d80a9d1a" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/phpSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/phpSample.cc.json index 7a01d5b953..0d0f969efc 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/phpSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/phpSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "f2b3bb71e1de6ce6dc2741bcf764eba5", + "checksum": "4fcabd3c3554395495ba2c6ef06a5b26", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "9fcad1339f6f0bcb" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json index 1c55da166d..7d35af4296 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/pythonSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "d260eb821ddacc56cbb5e4c31b1eca57", + "checksum": "dacc5c796a8858d2fe2a9a313da70ef6", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 2.0 }, "link": "", - "children": [] + "children": [], + "checksum": "fa939adf8e70c209" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/rubySample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/rubySample.cc.json index 2e11f3c56b..5de16817dd 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/rubySample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/rubySample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "104eed0fc76af7803a97a7629f04bdd9", + "checksum": "748506e963e404d131d4e44f5a8fa407", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 5.5 }, "link": "", - "children": [] + "children": [], + "checksum": "23c8c84736899d79" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json index bdc1ddb22b..aa3f42f50e 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/typescriptSample.cc.json @@ -1,5 +1,5 @@ { - "checksum": "d79ca8204b28294e18d75e97f058a844", + "checksum": "c0d233800e992e937d55a45b8f724992", "data": { "projectName": "", "nodes": [ @@ -33,7 +33,8 @@ "median_rloc_per_function": 2.0 }, "link": "", - "children": [] + "children": [], + "checksum": "87f41cfd0d94b349" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json index be7d7f71d2..ecf6558ce7 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/mergeResult.cc.json @@ -1,5 +1,5 @@ { - "checksum": "0cd1fad2d37f976279040567a010d627", + "checksum": "226b822b73458b554f47bdcf19337291", "data": { "projectName": "", "nodes": [ @@ -57,7 +57,8 @@ "median_rloc_per_function": 4.5 }, "link": "", - "children": [] + "children": [], + "checksum": "c40238c79d87defc" }, { "name": "hello.kt", @@ -83,7 +84,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "bfc329a4e973c424" } ] }, @@ -111,7 +113,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "39e5446f9451898e" }, { "name": "foo.py", @@ -137,7 +140,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "45b3835ff9d57197" }, { "name": "helloWorld.ts", @@ -163,7 +167,8 @@ "median_rloc_per_function": 1.0 }, "link": "", - "children": [] + "children": [], + "checksum": "e31a24ef7db12378" }, { "name": "whenCase.kt", @@ -189,7 +194,8 @@ "median_rloc_per_function": 6.0 }, "link": "", - "children": [] + "children": [], + "checksum": "91308e658fb97d1c" } ] } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json b/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json index 42fd212eb3..8f015a99c2 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json +++ b/analysis/analysers/parsers/UnifiedParser/src/test/resources/sampleProject.cc.json @@ -1,5 +1,5 @@ { - "checksum": "a6f4e0f34358cd3e5c430bb56b708464", + "checksum": "b0f79ba0968c65587ae202973bd72fa2", "data": { "projectName": "", "nodes": [ @@ -39,7 +39,8 @@ "median_rloc_per_function": 4.5 }, "link": "", - "children": [] + "children": [], + "checksum": "c40238c79d87defc" }, { "name": "hello.kt", @@ -65,7 +66,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "bfc329a4e973c424" } ] }, @@ -93,7 +95,8 @@ "median_rloc_per_function": 4.0 }, "link": "", - "children": [] + "children": [], + "checksum": "39e5446f9451898e" }, { "name": "foo.py", @@ -119,7 +122,8 @@ "median_rloc_per_function": 0.0 }, "link": "", - "children": [] + "children": [], + "checksum": "45b3835ff9d57197" }, { "name": "helloWorld.ts", @@ -145,7 +149,8 @@ "median_rloc_per_function": 1.0 }, "link": "", - "children": [] + "children": [], + "checksum": "e31a24ef7db12378" }, { "name": "whenCase.kt", @@ -171,7 +176,8 @@ "median_rloc_per_function": 6.0 }, "link": "", - "children": [] + "children": [], + "checksum": "91308e658fb97d1c" } ] } diff --git a/analysis/gradle/libs.versions.toml b/analysis/gradle/libs.versions.toml index 7f36cb2652..2d1649f76e 100644 --- a/analysis/gradle/libs.versions.toml +++ b/analysis/gradle/libs.versions.toml @@ -19,6 +19,7 @@ slf4j = "2.0.17" sonar-java = "6.8.0.23379" sonarqube = "6.3.1.5724" univocity-parsers = "2.9.1" +appmattus-crypto = "1.0.2" assertj = "3.27.6" junit5 = "5.14.0" junit-platform = "1.14.0" @@ -90,6 +91,7 @@ treesitter-go = { group = "io.github.bonede", name = "tree-sitter-go", version.r treesitter-php = { group = "io.github.bonede", name = "tree-sitter-php", version.ref = "tree-sitter-php"} treesitter-ruby = { group = "io.github.bonede", name = "tree-sitter-ruby", version.ref = "tree-sitter-ruby"} treesitter-bash = { group = "io.github.bonede", name = "tree-sitter-bash", version.ref = "tree-sitter-bash"} +appmattus-crypto = { group = "com.appmattus.crypto", name = "cryptohash", version.ref = "appmattus-crypto"} [plugins] sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } diff --git a/analysis/model/build.gradle.kts b/analysis/model/build.gradle.kts index 57b08c78a8..978ab16f88 100644 --- a/analysis/model/build.gradle.kts +++ b/analysis/model/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { implementation(libs.picocli) implementation(libs.slf4j.simple) implementation(libs.commons.text) + implementation(libs.appmattus.crypto) testImplementation(libs.kotlin.test) testImplementation(libs.junit.jupiter.api) diff --git a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculator.kt b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculator.kt new file mode 100644 index 0000000000..be3b82aab9 --- /dev/null +++ b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculator.kt @@ -0,0 +1,24 @@ +package de.maibornwolff.codecharta.model + +import com.appmattus.crypto.Algorithm + +class ChecksumCalculator { + companion object { + fun calculateChecksum(fileContent: String): String? { + if (fileContent.isEmpty()) { + return null + } + val hash = Algorithm.XXHash64().hash(fileContent.toByteArray()) + return convertToHexString(hash) + } + + private fun convertToHexString(bytes: ByteArray): String { + // Convert each byte to a 2-character hexadecimal string (e.g. byte value 255 becomes ff) + val hexChars = bytes.map { byte -> + val hexValue = byte.toInt() and 0xFF // Convert to unsigned integer in range 0-255 + hexValue.toString(16).padStart(2, '0') // Convert to hex and pad with zero if needed + } + return hexChars.joinToString("") + } + } +} diff --git a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/MutableNode.kt b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/MutableNode.kt index 1d940488c7..d0c8599dd3 100644 --- a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/MutableNode.kt +++ b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/MutableNode.kt @@ -11,7 +11,8 @@ class MutableNode( var attributes: Map = mapOf(), val link: String? = "", childrenList: Set = setOf(), - @Transient val nodeMergingStrategy: NodeMergerStrategy = NodeMaxAttributeMerger() + @Transient val nodeMergingStrategy: NodeMergerStrategy = NodeMaxAttributeMerger(), + val checksum: String? = null ) : Tree() { override var children = childrenList.toMutableSet() @@ -44,12 +45,11 @@ class MutableNode( } } } - attributes = - attributes.mapKeys { - metricNameTranslator.translate(it.key) - }.filterKeys { - it.isNotBlank() - } + attributes = attributes.mapKeys { + metricNameTranslator.translate(it.key) + }.filterKeys { + it.isNotBlank() + } return this } @@ -60,10 +60,8 @@ class MutableNode( type, attributes, link, - children = - children.map { - it.toNode() - }.toSet() + children = children.map { it.toNode() }.toSet(), + checksum = checksum ) } diff --git a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/Node.kt b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/Node.kt index 87d7cbee92..6dc4ad6c55 100644 --- a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/Node.kt +++ b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/Node.kt @@ -7,7 +7,8 @@ class Node( val type: NodeType? = NodeType.File, val attributes: Map = mapOf(), val link: String? = "", - override val children: Set = setOf() + override val children: Set = setOf(), + val checksum: String? = null ) : Tree() { override fun getPathOfChild(child: Tree): Path { if (!children.contains(child)) { @@ -34,9 +35,8 @@ class Node( type, attributes, link, - children.map { - it.toMutableNode() - }.toSet() + children.map { it.toMutableNode() }.toSet(), + checksum = checksum ) } } diff --git a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ProjectBuilder.kt b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ProjectBuilder.kt index bb3a018690..90e9e8ede8 100644 --- a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ProjectBuilder.kt +++ b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/model/ProjectBuilder.kt @@ -13,11 +13,7 @@ open class ProjectBuilder( private var blacklist: MutableList = mutableListOf() ) { init { - if (nodes.size != 1) { - throw IllegalStateException( - "No unique root node was found, instead ${nodes.size} candidates identified." - ) - } + check(nodes.size == 1) { "No unique root node was found, instead ${nodes.size} candidates identified." } } val rootNode: MutableNode @@ -81,12 +77,9 @@ open class ProjectBuilder( Project( edges = edges.toList(), blacklist = blacklist.toList(), - projectName = Companion.DUMMY_PROJECT_NAME, + projectName = DUMMY_PROJECT_NAME, attributeTypes = attributeTypes.toMap(), - nodes = - nodes.map { - it.toNode() - }.toList(), + nodes = nodes.map { it.toNode() }.toList(), attributeDescriptors = attributeDescriptors.toMap() ) diff --git a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/serialization/NodeJsonDeserializer.kt b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/serialization/NodeJsonDeserializer.kt index e8ed3e563c..f54751f72e 100644 --- a/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/serialization/NodeJsonDeserializer.kt +++ b/analysis/model/src/main/kotlin/de/maibornwolff/codecharta/serialization/NodeJsonDeserializer.kt @@ -19,8 +19,9 @@ internal class NodeJsonDeserializer : JsonDeserializer { val attributes = deserializeAttributes(jsonNode) val link = deserializeLink(jsonNode) val children = deserializeChildren(jsonNode) + val checksum = deserializeChecksum(jsonNode) - return Node(name, nodeType, attributes, link, children.toSet()) + return Node(name, nodeType, attributes, link, children.toSet(), checksum) } private fun deserializeLink(jsonNode: JsonObject): String? { @@ -58,4 +59,8 @@ internal class NodeJsonDeserializer : JsonDeserializer { deserialize(it, Node::class.java, null) } } + + private fun deserializeChecksum(jsonNode: JsonObject): String? { + return jsonNode.get("checksum")?.asString + } } diff --git a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculatorTest.kt b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculatorTest.kt new file mode 100644 index 0000000000..aec32e4333 --- /dev/null +++ b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ChecksumCalculatorTest.kt @@ -0,0 +1,120 @@ +package de.maibornwolff.codecharta.model + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class ChecksumCalculatorTest { + @Test + fun `should return null for empty file content`() { + val checksum = ChecksumCalculator.calculateChecksum("") + assertNull(checksum) + } + + @Test + fun `should produce same checksum for identical file content`() { + val content1 = "public class Test {\n void method() {}\n}" + val content2 = "public class Test {\n void method() {}\n}" + + val checksum1 = ChecksumCalculator.calculateChecksum(content1) + val checksum2 = ChecksumCalculator.calculateChecksum(content2) + + assertEquals(checksum1, checksum2) + } + + @Test + fun `should produce different checksums for different file content`() { + val content1 = "public class Test {\n void method() {}\n}" + val content2 = "public class Test {\n void method2() {}\n}" + + val checksum1 = ChecksumCalculator.calculateChecksum(content1) + val checksum2 = ChecksumCalculator.calculateChecksum(content2) + + assertNotEquals(checksum1, checksum2) + } + + @Test + fun `should produce different checksums for content with different whitespace`() { + val content1 = "public class Test {\n void method() {}\n}" + val content2 = "public class Test {\n void method() {}\n}" + + val checksum1 = ChecksumCalculator.calculateChecksum(content1) + val checksum2 = ChecksumCalculator.calculateChecksum(content2) + + assertNotEquals(checksum1, checksum2, "Checksums should differ when whitespace differs") + } + + @Test + fun `should handle multi-line content`() { + val content = """ + public class Example { + private int value; + + public Example(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + """.trimIndent() + + val checksum = ChecksumCalculator.calculateChecksum(content) + + assert(checksum != null) + assert(checksum!!.isNotEmpty()) + } + + @Test + fun `should handle special characters`() { + val content = "// Special chars: Àâü ß € @ # $ % & * ( ) [ ] { } < > | \\ / ? ! ~ ` ' \"" + + val checksum = ChecksumCalculator.calculateChecksum(content) + + assert(checksum != null) + assert(checksum!!.isNotEmpty()) + } + + @Test + fun `should produce different checksums when content is modified`() { + val content1 = "function test() { return 42; }" + val content2 = "function test() { return 43; }" + + val checksum1 = ChecksumCalculator.calculateChecksum(content1) + val checksum2 = ChecksumCalculator.calculateChecksum(content2) + + assertNotEquals(checksum1, checksum2) + } + + @Test + fun `should produce hexadecimal string`() { + val content = "test content" + + val checksum = ChecksumCalculator.calculateChecksum(content) + + assert(checksum != null) + assert(checksum!!.matches(Regex("^[0-9a-f]+$"))) + } + + @Test + fun `should handle very long content`() { + val content = "a".repeat(10000) + + val checksum = ChecksumCalculator.calculateChecksum(content) + + assert(checksum != null) + assert(checksum!!.isNotEmpty()) + } + + @Test + fun `should handle unicode content`() { + val content = "Hello δΈ–η•Œ Ω…Ψ±Ψ­Ψ¨Ψ§ ΠΌΠΈΡ€ 🌍" + + val checksum = ChecksumCalculator.calculateChecksum(content) + + assert(checksum != null) + assert(checksum!!.isNotEmpty()) + } +} diff --git a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ProjectBuilderTest.kt b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ProjectBuilderTest.kt index e6ce105c61..508caed534 100644 --- a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ProjectBuilderTest.kt +++ b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/model/ProjectBuilderTest.kt @@ -24,7 +24,7 @@ class ProjectBuilderTest { @Test fun `it should insert a new node as child of root when there is no root node `() { -// when + // when val projectBuilder = ProjectBuilder() val nodeForInsertion = MutableNode("someNode", NodeType.File) projectBuilder.insertByPath(Path.trivialPath(), nodeForInsertion) @@ -37,7 +37,7 @@ class ProjectBuilderTest { @Test fun `it should create a project with root when inserting a new node into a project with root-node`() { -// when + // when val root = MutableNode("root", NodeType.Folder) val projectBuilder = ProjectBuilder(listOf(root)) @@ -53,7 +53,7 @@ class ProjectBuilderTest { @Test fun `it should filter out empty folders`() { -// when + // when val projectBuilder = ProjectBuilder() val nodeForInsertion = MutableNode("someNode", NodeType.Folder) projectBuilder.insertByPath(Path.trivialPath(), nodeForInsertion) @@ -67,7 +67,7 @@ class ProjectBuilderTest { @Test fun `it should add the correct attribute-types`() { -// when + // when val projectBuilder = ProjectBuilder( attributeTypes = mutableMapOf("nodes" to mutableMapOf("nodeMetric" to AttributeType.ABSOLUTE)) @@ -273,7 +273,7 @@ class ProjectBuilderTest { @Test fun `it should print the correct content keys`() { -// when + // when val projectBuilder = ProjectBuilder() // then @@ -284,4 +284,39 @@ class ProjectBuilderTest { "blacklist=[]" ) } + + @Test + fun `it should preserve checksums through the build process`() { + // given + val sampleChecksum = "abc123def456" + val rootNode = MutableNode("root", NodeType.Folder) + val fileWithChecksum = + MutableNode( + name = "TestFile.java", + type = NodeType.File, + attributes = mapOf("rloc" to 100, "complexity" to 15), + checksum = sampleChecksum + ) + val fileWithoutChecksum = + MutableNode( + name = "EmptyFile.java", + type = NodeType.File, + attributes = mapOf("rloc" to 0), + checksum = null + ) + + rootNode.children.add(fileWithChecksum) + rootNode.children.add(fileWithoutChecksum) + + // when + val projectBuilder = ProjectBuilder(listOf(rootNode)) + val project = projectBuilder.build() + + // then + val builtFileWithChecksum = project.rootNode.children.find { it.name == "TestFile.java" } + val builtFileWithoutChecksum = project.rootNode.children.find { it.name == "EmptyFile.java" } + + assertThat(builtFileWithChecksum!!.checksum).isEqualTo(sampleChecksum) + assertThat(builtFileWithoutChecksum!!.checksum).isNull() + } } diff --git a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectDeserializerTest.kt b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectDeserializerTest.kt index ad6db029ac..50972dd178 100644 --- a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectDeserializerTest.kt +++ b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectDeserializerTest.kt @@ -123,4 +123,36 @@ class ProjectDeserializerTest { // then assertThat(project).isNull() } + + @Test + fun `should deserialize checksum from json`() { + // given + val sampleChecksum = "abc123def456" + val jsonWithChecksum = """ + { + "projectName": "TestProject", + "apiVersion": "1.3", + "nodes": [ + { + "name": "root", + "type": "Folder", + "children": [ + { + "name": "TestFile.java", + "type": "File", + "attributes": {"rloc": 100}, + "checksum": "$sampleChecksum" + } + ] + } + ] + } + """.trimIndent() + + // when + val project = ProjectDeserializer.deserializeProject(jsonWithChecksum) + + // then + assertThat(project.rootNode.children.first().checksum).isEqualTo(sampleChecksum) + } } diff --git a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectSerializerTest.kt b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectSerializerTest.kt index 910c5057ff..284cfbaa21 100644 --- a/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectSerializerTest.kt +++ b/analysis/model/src/test/kotlin/de/maibornwolff/codecharta/serialization/ProjectSerializerTest.kt @@ -1,8 +1,10 @@ package de.maibornwolff.codecharta.serialization +import de.maibornwolff.codecharta.model.MutableNode +import de.maibornwolff.codecharta.model.NodeType import de.maibornwolff.codecharta.model.Project +import de.maibornwolff.codecharta.model.ProjectBuilder import de.maibornwolff.codecharta.util.Logger -import io.github.oshai.kotlinlogging.KLogger import io.mockk.called import io.mockk.every import io.mockk.mockk @@ -30,7 +32,6 @@ class ProjectSerializerTest { private val tempDir = createTempDirectory() private val filename = tempDir.absolute().toString() + "test.cc.json" private val project = mockk() - private val loggerMock = mockk() private val lambdaSlot = mutableListOf<() -> String>() companion object { @@ -139,4 +140,27 @@ class ProjectSerializerTest { // then Assertions.assertThat(lambdaSlot.last()().endsWith(absoluteOutputFilePath)).isTrue() } + + @Test + fun `should include checksum in serialized json`() { + // given + val sampleChecksum = "abc123def456" + val rootNode = MutableNode("root", NodeType.Folder) + val fileNode = MutableNode( + name = "TestFile.java", + type = NodeType.File, + attributes = mapOf("rloc" to 100), + checksum = sampleChecksum + ) + rootNode.children.add(fileNode) + val testProject = ProjectBuilder(listOf(rootNode)).build() + + // when + val stream = ByteArrayOutputStream() + ProjectSerializer.serializeProject(testProject, stream, false) + val serializedJson = stream.toString("UTF-8") + + // then + Assertions.assertThat(serializedJson).contains("\"checksum\":\"$sampleChecksum\"") + } } diff --git a/gh-pages/_docs/05-parser/02-raw-text.md b/gh-pages/_docs/05-parser/02-raw-text.md index dcc5969055..28a2048ac6 100644 --- a/gh-pages/_docs/05-parser/02-raw-text.md +++ b/gh-pages/_docs/05-parser/02-raw-text.md @@ -25,7 +25,7 @@ This parser analyzes code, regardless of the programming language, to generate t | Parameter | Description | |-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `FILE or FOLDER` | file/project to parseProject | -| `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | +| `-bf, --base-file=` | base cc.json file with checksums to skip unchanged files during analysis | | `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | | `-fe, --file-extensions=` | comma-separated list of file-extensions to parse only those files (default: any) | | `-h, --help` | displays this help and exits | @@ -39,6 +39,7 @@ This parser analyzes code, regardless of the programming language, to generate t ``` Usage: ccsh rawtextparser [-h] [-nc] [--verbose] [--without-default-excludes] + [-bf=] [--max-indentation-level=] [-o=] [--tab-width=] [-e=]... [-fe=]... diff --git a/gh-pages/_docs/05-parser/05-unified.md b/gh-pages/_docs/05-parser/05-unified.md index e77272b77d..2114db9047 100644 --- a/gh-pages/_docs/05-parser/05-unified.md +++ b/gh-pages/_docs/05-parser/05-unified.md @@ -56,6 +56,7 @@ Some metrics are calculated on a per-function basis rather than per-file. Each o | Parameter | Description | |-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| | `FOLDER or FILE` | The project folder or code file to parse. To merge the result with an existing project piped into STDIN, pass a '-' as an additional argument | +| `-bf, --base-file=` | base cc.json file with checksums to skip unchanged files during analysis | | `-e, --exclude=` | comma-separated list of regex patterns to exclude files/folders | | `-fe, --file-extensions=` | comma-separated list of file-extensions to parse only those files (default: any) | | `-h, --help` | displays this help and exits | @@ -65,9 +66,9 @@ Some metrics are calculated on a per-function basis rather than per-file. Each o | `--verbose` | displays messages about parsed and ignored files | ``` -Usage: ccsh unifiedparser [-h] [-ibf] [-nc] [--verbose] [-o=] - [-e=]... - [-fe=]... FILE or FOLDER... +Usage: ccsh unifiedparser [-h] [-ibf] [-nc] [--verbose] [-bf=] + [-o=] [-e=]... + [-fe=]... FILE or FOLDER.. ``` ## Examples