Skip to content

Commit c954141

Browse files
MoleratUlrike Kiesel
andauthored
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 <ulrike.kiesel@maibornwolff.de>
1 parent 4fd2a8e commit c954141

38 files changed

Lines changed: 873 additions & 109 deletions

analysis/.kotlin/sessions/kotlin-compiler-12322078610956747093.salive

Whitespace-only changes.

analysis/analysers/parsers/UnifiedParser/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The Unified Parser is parser to generate code metrics from a source code file or
1010
|--------------|----------------------------------------|
1111
| Javascript | .js, .cjs, .mjs |
1212
| Typescript | .ts, .cts, .mts |
13+
| TSX | .tsx |
1314
| Java | .java |
1415
| Kotlin | .kt |
1516
| C# | .cs |
@@ -22,6 +23,7 @@ The Unified Parser is parser to generate code metrics from a source code file or
2223
| Ruby | .rb |
2324
| Swift | .swift |
2425
| Bash | .sh |
26+
| Vue | .vue |
2527

2628
## Supported Metrics
2729

analysis/analysers/parsers/UnifiedParser/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ dependencies {
99
implementation(libs.kotter.test)
1010

1111
// TreesitterLibrary provides all TreeSitter dependencies and metric calculation
12-
implementation("com.github.MaibornWolff:TreeSitterExcavationSite:v0.2.0")
12+
implementation("com.github.MaibornWolff:TreeSitterExcavationSite:v0.4.1")
1313

1414
testImplementation(libs.jsonassert)
1515
}

analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/AttributeDescriptors.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ internal fun getAttributeDescriptors(): Map<String, AttributeDescriptor> {
9494
),
9595
"mean_complexity_per_function" to AttributeDescriptor(
9696
title = "Mean complexity per function",
97-
description = "The mean complexity found in the body of a function of this file."
97+
description = "The mean complexity found in the body of a function of this file.",
98+
link = ghLink,
99+
direction = -1,
100+
analyzers = analyzerName
98101
),
99102
"median_complexity_per_function" to AttributeDescriptor(
100103
title = "Median complexity per function",

analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors
33
import de.maibornwolff.codecharta.serialization.FileExtension
44
import de.maibornwolff.treesitter.excavationsite.api.Language
55

6-
enum class AvailableCollectors(val fileExtension: FileExtension, val collectorFactory: () -> TreeSitterLibraryCollector) {
6+
/** Maps each supported [FileExtension] to a factory for the corresponding [TreeSitterLibraryCollector]. */
7+
enum class AvailableCollectors(
8+
/** The file extension this collector handles. */
9+
val fileExtension: FileExtension,
10+
/** Factory function that creates the collector for this language. */
11+
val collectorFactory: () -> TreeSitterLibraryCollector
12+
) {
713
TYPESCRIPT(FileExtension.TYPESCRIPT, { TreeSitterLibraryCollector(Language.TYPESCRIPT) }),
14+
TSX(FileExtension.TSX, { TreeSitterLibraryCollector(Language.TSX) }),
815
JAVASCRIPT(FileExtension.JAVASCRIPT, { TreeSitterLibraryCollector(Language.JAVASCRIPT) }),
916
KOTLIN(FileExtension.KOTLIN, { TreeSitterLibraryCollector(Language.KOTLIN) }),
1017
OBJECTIVE_C(FileExtension.OBJECTIVE_C, { TreeSitterLibraryCollector(Language.OBJECTIVE_C) }),

analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ object TreeSitterAdapter {
5151
FileExtension.JAVA -> Language.JAVA
5252
FileExtension.KOTLIN -> Language.KOTLIN
5353
FileExtension.TYPESCRIPT -> Language.TYPESCRIPT
54+
FileExtension.TSX -> Language.TSX
5455
FileExtension.JAVASCRIPT -> Language.JAVASCRIPT
5556
FileExtension.PYTHON -> Language.PYTHON
5657
FileExtension.GO -> Language.GO

analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/UnifiedParserTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class UnifiedParserTest {
4343
Arguments.of("ruby", ".rb"),
4444
Arguments.of("swift", ".swift"),
4545
Arguments.of("typescript", ".ts"),
46+
Arguments.of("tsx", ".tsx"),
4647
Arguments.of("vue", ".vue")
4748
)
4849
}

analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TreeSitterAdapterTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class TreeSitterAdapterTest {
6464
FileExtension.JAVA to Language.JAVA,
6565
FileExtension.KOTLIN to Language.KOTLIN,
6666
FileExtension.TYPESCRIPT to Language.TYPESCRIPT,
67+
FileExtension.TSX to Language.TSX,
6768
FileExtension.JAVASCRIPT to Language.JAVASCRIPT,
6869
FileExtension.PYTHON to Language.PYTHON,
6970
FileExtension.GO to Language.GO,
@@ -143,6 +144,7 @@ class TreeSitterAdapterTest {
143144
FileExtension.JAVA,
144145
FileExtension.KOTLIN,
145146
FileExtension.TYPESCRIPT,
147+
FileExtension.TSX,
146148
FileExtension.JAVASCRIPT,
147149
FileExtension.PYTHON,
148150
FileExtension.GO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors
2+
3+
import de.maibornwolff.treesitter.excavationsite.api.AvailableFileMetrics
4+
import de.maibornwolff.treesitter.excavationsite.api.Language
5+
import org.assertj.core.api.Assertions.assertThat
6+
import org.junit.jupiter.api.Test
7+
import java.io.File
8+
9+
class TsxCollectorTest {
10+
private val collector = TreeSitterLibraryCollector(Language.TSX)
11+
12+
private fun createTestFile(content: String): File {
13+
val tempFile = File.createTempFile("testFile", ".tsx")
14+
tempFile.writeText(content)
15+
tempFile.deleteOnExit()
16+
return tempFile
17+
}
18+
19+
@Test
20+
fun `should parse a minimal tsx component and return non-zero loc`() {
21+
// Arrange
22+
val fileContent = """
23+
function Hello() {
24+
return <div>Hello World</div>;
25+
}
26+
""".trimIndent()
27+
val input = createTestFile(fileContent)
28+
29+
// Act
30+
val result = collector.collectMetricsForFile(input)
31+
32+
// Assert
33+
assertThat(result.attributes[AvailableFileMetrics.LINES_OF_CODE.metricName] as Double).isGreaterThan(0.0)
34+
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName] as Double).isEqualTo(1.0)
35+
}
36+
37+
@Test
38+
fun `should count ternary operator inside JSX expression for complexity`() {
39+
// Arrange
40+
val fileContent = """const element = (<h1>{x < 10 ? "Banana" : "Apple"}</h1>);"""
41+
val input = createTestFile(fileContent)
42+
43+
// Act
44+
val result = collector.collectMetricsForFile(input)
45+
46+
// Assert
47+
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
48+
}
49+
50+
@Test
51+
fun `should count logical AND short-circuit in JSX expression for complexity`() {
52+
// Arrange
53+
val fileContent = """const element = (<div>{isLoggedIn && <Dashboard />}</div>);"""
54+
val input = createTestFile(fileContent)
55+
56+
// Act
57+
val result = collector.collectMetricsForFile(input)
58+
59+
// Assert
60+
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
61+
}
62+
63+
@Test
64+
fun `should not count inline arrow function in JSX prop as a function`() {
65+
// Arrange
66+
val fileContent = """
67+
function App() {
68+
return (<Button onClick={() => doStuff()} />);
69+
}
70+
""".trimIndent()
71+
val input = createTestFile(fileContent)
72+
73+
// Act
74+
val result = collector.collectMetricsForFile(input)
75+
76+
// Assert
77+
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
78+
}
79+
80+
@Test
81+
fun `should count arrow function component assigned to const as a function`() {
82+
// Arrange
83+
val fileContent = """
84+
const Fruit = () => (
85+
<div>Hello</div>
86+
);
87+
""".trimIndent()
88+
val input = createTestFile(fileContent)
89+
90+
// Act
91+
val result = collector.collectMetricsForFile(input)
92+
93+
// Assert
94+
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
95+
}
96+
97+
@Test
98+
fun `should count logical OR short-circuit in JSX expression for complexity`() {
99+
// Arrange
100+
val fileContent = """const element = (<div>{error || <Content />}</div>);"""
101+
val input = createTestFile(fileContent)
102+
103+
// Act
104+
val result = collector.collectMetricsForFile(input)
105+
106+
// Assert
107+
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
108+
}
109+
110+
@Test
111+
fun `should accumulate complexity across multiple JSX conditional expressions`() {
112+
// Arrange
113+
val fileContent = """
114+
function Card({ isAdmin, isActive, role }) {
115+
return (
116+
<div>
117+
{isAdmin && <AdminBadge />}
118+
{isActive ? <ActiveIcon /> : <InactiveIcon />}
119+
{role === "guest" || <Content />}
120+
</div>
121+
);
122+
}
123+
""".trimIndent()
124+
val input = createTestFile(fileContent)
125+
126+
// Act
127+
val result = collector.collectMetricsForFile(input)
128+
129+
// Assert
130+
// 1 (function) + 1 (&&) + 1 (ternary) + 1 (||) = 4
131+
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(4.0)
132+
}
133+
134+
@Test
135+
fun `should not count map callback returning JSX as a function`() {
136+
// Arrange
137+
val fileContent = """
138+
function List({ items }) {
139+
return (<ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>);
140+
}
141+
""".trimIndent()
142+
val input = createTestFile(fileContent)
143+
144+
// Act
145+
val result = collector.collectMetricsForFile(input)
146+
147+
// Assert
148+
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
149+
}
150+
151+
@Test
152+
fun `should count nullish coalescing operator in JSX expression for complexity`() {
153+
// Arrange
154+
val fileContent = """const element = (<div>{value ?? <Fallback />}</div>);"""
155+
val input = createTestFile(fileContent)
156+
157+
// Act
158+
val result = collector.collectMetricsForFile(input)
159+
160+
// Assert
161+
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
162+
}
163+
164+
@Test
165+
fun `should count JSX block comment in comment lines`() {
166+
// Arrange
167+
val fileContent = """
168+
function App() {
169+
return (
170+
<div>
171+
{/* this is a JSX comment */}
172+
<p>Hello</p>
173+
</div>
174+
);
175+
}
176+
""".trimIndent()
177+
val input = createTestFile(fileContent)
178+
179+
// Act
180+
val result = collector.collectMetricsForFile(input)
181+
182+
// Assert
183+
assertThat(result.attributes[AvailableFileMetrics.COMMENT_LINES.metricName]).isEqualTo(1.0)
184+
}
185+
}

analysis/analysers/parsers/UnifiedParser/src/test/resources/empty.cc.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"checksum": "f983aebcae5f795440bf7a8ec6cadba1",
32
"data": {
43
"projectName": "",
54
"nodes": [
@@ -152,9 +151,11 @@
152151
"description": "The mean complexity found in the body of a function of this file.",
153152
"hintLowValue": "",
154153
"hintHighValue": "",
155-
"link": "",
154+
"link": "https://codecharta.com/docs/parser/unified",
156155
"direction": -1,
157-
"analyzers": []
156+
"analyzers": [
157+
"unifiedParser"
158+
]
158159
},
159160
"median_complexity_per_function": {
160161
"title": "Median complexity per function",
@@ -268,5 +269,6 @@
268269
}
269270
},
270271
"blacklist": []
271-
}
272+
},
273+
"checksum": "cbe157f368d090f1a5105f10338e9ff7"
272274
}

0 commit comments

Comments
 (0)