From c95414153d2e9e4e796e7782a461e7de11965314 Mon Sep 17 00:00:00 2001 From: Molerat Date: Mon, 20 Apr 2026 10:10:31 +0200 Subject: [PATCH 1/2] Add TSX support to UnifiedParser (#4466) * chore(deps): update dependency TreesitterExcavationSite to v0.4.1 v0.3.0 of the library changed LOC counting (trailing newlines no longer counted), causing off-by-1 diffs in golden files * feat(analysis): add TSX language support to UnifiedParser * fix(analysis): replace fixed sleep with polling in ProjectInputReader The fixed 100ms sleep was insufficient on CI after the TreeSitter v0.4.1 dependency bump (1.5 MB JAR), which slightly increased JVM startup time. The upstream svnlogparser process no longer wrote its sync flag within 100ms, so modify read an empty stdin and skipped writing the output file. Replace with a polling loop (50ms interval, 500ms max) that exits as soon as data is available, making the pipe chain robust to JVM startup variance. * fix(analysis): resolve SonarCloud quality gate failures for TSX support * fix(analysis): complete mean_complexity_per_function descriptor in tsx fixture * fix(analysis): add missing link and analyzers to mean_complexity_per_function descriptor --------- Co-authored-by: Ulrike Kiesel --- ...otlin-compiler-12322078610956747093.salive | 0 .../analysers/parsers/UnifiedParser/README.md | 2 + .../parsers/UnifiedParser/build.gradle.kts | 2 +- .../parsers/unified/AttributeDescriptors.kt | 5 +- .../metriccollectors/AvailableCollectors.kt | 9 +- .../metriccollectors/TreeSitterAdapter.kt | 1 + .../parsers/unified/UnifiedParserTest.kt | 1 + .../metriccollectors/TreeSitterAdapterTest.kt | 2 + .../metriccollectors/TsxCollectorTest.kt | 185 +++++++++++ .../src/test/resources/empty.cc.json | 10 +- .../src/test/resources/excludePattern.cc.json | 12 +- .../src/test/resources/includeAll.cc.json | 14 +- .../src/test/resources/kotlinOnly.cc.json | 10 +- .../languageSamples/bashSample.cc.json | 10 +- .../languageSamples/cHeaderSample.cc.json | 10 +- .../resources/languageSamples/cSample.cc.json | 10 +- .../languageSamples/cSharpSample.cc.json | 10 +- .../languageSamples/cppHeaderSample.cc.json | 10 +- .../languageSamples/cppSample.cc.json | 10 +- .../languageSamples/goSample.cc.json | 10 +- .../languageSamples/javaSample.cc.json | 10 +- .../languageSamples/javascriptSample.cc.json | 10 +- .../languageSamples/kotlinSample.cc.json | 10 +- .../languageSamples/objectiveCSample.cc.json | 10 +- .../languageSamples/phpSample.cc.json | 10 +- .../languageSamples/pythonSample.cc.json | 10 +- .../languageSamples/rubySample.cc.json | 10 +- .../languageSamples/swiftSample.cc.json | 10 +- .../languageSamples/tsxSample.cc.json | 307 ++++++++++++++++++ .../resources/languageSamples/tsxSample.tsx | 109 +++++++ .../languageSamples/typescriptSample.cc.json | 10 +- .../languageSamples/vueSample.cc.json | 12 +- .../src/test/resources/mergeResult.cc.json | 12 +- .../src/test/resources/sampleProject.cc.json | 14 +- .../codecharta/serialization/FileExtension.kt | 9 +- .../serialization/ProjectInputReader.kt | 17 +- gh-pages/_docs/05-parser/05-unified.md | 24 +- plans/add-tsx-support.md | 65 ++++ 38 files changed, 873 insertions(+), 109 deletions(-) create mode 100644 analysis/.kotlin/sessions/kotlin-compiler-12322078610956747093.salive create mode 100644 analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt create mode 100644 analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/tsxSample.cc.json create mode 100644 analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/tsxSample.tsx create mode 100644 plans/add-tsx-support.md diff --git a/analysis/.kotlin/sessions/kotlin-compiler-12322078610956747093.salive b/analysis/.kotlin/sessions/kotlin-compiler-12322078610956747093.salive new file mode 100644 index 0000000000..e69de29bb2 diff --git a/analysis/analysers/parsers/UnifiedParser/README.md b/analysis/analysers/parsers/UnifiedParser/README.md index cd33a39813..2571dd1204 100644 --- a/analysis/analysers/parsers/UnifiedParser/README.md +++ b/analysis/analysers/parsers/UnifiedParser/README.md @@ -10,6 +10,7 @@ The Unified Parser is parser to generate code metrics from a source code file or |--------------|----------------------------------------| | Javascript | .js, .cjs, .mjs | | Typescript | .ts, .cts, .mts | +| TSX | .tsx | | Java | .java | | Kotlin | .kt | | C# | .cs | @@ -22,6 +23,7 @@ The Unified Parser is parser to generate code metrics from a source code file or | Ruby | .rb | | Swift | .swift | | Bash | .sh | +| Vue | .vue | ## Supported Metrics diff --git a/analysis/analysers/parsers/UnifiedParser/build.gradle.kts b/analysis/analysers/parsers/UnifiedParser/build.gradle.kts index e0511d0295..cfe57de0db 100644 --- a/analysis/analysers/parsers/UnifiedParser/build.gradle.kts +++ b/analysis/analysers/parsers/UnifiedParser/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { implementation(libs.kotter.test) // TreesitterLibrary provides all TreeSitter dependencies and metric calculation - implementation("com.github.MaibornWolff:TreeSitterExcavationSite:v0.2.0") + implementation("com.github.MaibornWolff:TreeSitterExcavationSite:v0.4.1") testImplementation(libs.jsonassert) } diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/AttributeDescriptors.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/AttributeDescriptors.kt index c094f8583f..dcef105db5 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/AttributeDescriptors.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/AttributeDescriptors.kt @@ -94,7 +94,10 @@ internal fun getAttributeDescriptors(): Map { ), "mean_complexity_per_function" to AttributeDescriptor( title = "Mean complexity per function", - description = "The mean complexity found in the body of a function of this file." + description = "The mean complexity found in the body of a function of this file.", + link = ghLink, + direction = -1, + analyzers = analyzerName ), "median_complexity_per_function" to AttributeDescriptor( title = "Median complexity per function", diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt index d039363e80..e6bb56396a 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt @@ -3,8 +3,15 @@ package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors import de.maibornwolff.codecharta.serialization.FileExtension import de.maibornwolff.treesitter.excavationsite.api.Language -enum class AvailableCollectors(val fileExtension: FileExtension, val collectorFactory: () -> TreeSitterLibraryCollector) { +/** Maps each supported [FileExtension] to a factory for the corresponding [TreeSitterLibraryCollector]. */ +enum class AvailableCollectors( + /** The file extension this collector handles. */ + val fileExtension: FileExtension, + /** Factory function that creates the collector for this language. */ + val collectorFactory: () -> TreeSitterLibraryCollector +) { TYPESCRIPT(FileExtension.TYPESCRIPT, { TreeSitterLibraryCollector(Language.TYPESCRIPT) }), + TSX(FileExtension.TSX, { TreeSitterLibraryCollector(Language.TSX) }), JAVASCRIPT(FileExtension.JAVASCRIPT, { TreeSitterLibraryCollector(Language.JAVASCRIPT) }), KOTLIN(FileExtension.KOTLIN, { TreeSitterLibraryCollector(Language.KOTLIN) }), OBJECTIVE_C(FileExtension.OBJECTIVE_C, { TreeSitterLibraryCollector(Language.OBJECTIVE_C) }), diff --git a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapter.kt b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapter.kt index 5b408e29c7..1dd9def2ea 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapter.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapter.kt @@ -51,6 +51,7 @@ object TreeSitterAdapter { FileExtension.JAVA -> Language.JAVA FileExtension.KOTLIN -> Language.KOTLIN FileExtension.TYPESCRIPT -> Language.TYPESCRIPT + FileExtension.TSX -> Language.TSX FileExtension.JAVASCRIPT -> Language.JAVASCRIPT FileExtension.PYTHON -> Language.PYTHON FileExtension.GO -> Language.GO 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 0d8a3807ad..1f580cd2e4 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 @@ -43,6 +43,7 @@ class UnifiedParserTest { Arguments.of("ruby", ".rb"), Arguments.of("swift", ".swift"), Arguments.of("typescript", ".ts"), + Arguments.of("tsx", ".tsx"), Arguments.of("vue", ".vue") ) } diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapterTest.kt b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapterTest.kt index 03deeb949f..95d64f1e93 100644 --- a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapterTest.kt +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapterTest.kt @@ -64,6 +64,7 @@ class TreeSitterAdapterTest { FileExtension.JAVA to Language.JAVA, FileExtension.KOTLIN to Language.KOTLIN, FileExtension.TYPESCRIPT to Language.TYPESCRIPT, + FileExtension.TSX to Language.TSX, FileExtension.JAVASCRIPT to Language.JAVASCRIPT, FileExtension.PYTHON to Language.PYTHON, FileExtension.GO to Language.GO, @@ -143,6 +144,7 @@ class TreeSitterAdapterTest { FileExtension.JAVA, FileExtension.KOTLIN, FileExtension.TYPESCRIPT, + FileExtension.TSX, FileExtension.JAVASCRIPT, FileExtension.PYTHON, FileExtension.GO, diff --git a/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt new file mode 100644 index 0000000000..ee9e458260 --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt @@ -0,0 +1,185 @@ +package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors + +import de.maibornwolff.treesitter.excavationsite.api.AvailableFileMetrics +import de.maibornwolff.treesitter.excavationsite.api.Language +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.io.File + +class TsxCollectorTest { + private val collector = TreeSitterLibraryCollector(Language.TSX) + + private fun createTestFile(content: String): File { + val tempFile = File.createTempFile("testFile", ".tsx") + tempFile.writeText(content) + tempFile.deleteOnExit() + return tempFile + } + + @Test + fun `should parse a minimal tsx component and return non-zero loc`() { + // Arrange + val fileContent = """ + function Hello() { + return
Hello World
; + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + assertThat(result.attributes[AvailableFileMetrics.LINES_OF_CODE.metricName] as Double).isGreaterThan(0.0) + assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName] as Double).isEqualTo(1.0) + } + + @Test + fun `should count ternary operator inside JSX expression for complexity`() { + // Arrange + val fileContent = """const element = (

{x < 10 ? "Banana" : "Apple"}

);""" + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0) + } + + @Test + fun `should count logical AND short-circuit in JSX expression for complexity`() { + // Arrange + val fileContent = """const element = (
{isLoggedIn && }
);""" + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0) + } + + @Test + fun `should not count inline arrow function in JSX prop as a function`() { + // Arrange + val fileContent = """ + function App() { + return (