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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
2 changes: 2 additions & 0 deletions analysis/analysers/parsers/UnifiedParser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion analysis/analysers/parsers/UnifiedParser/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ internal fun getAttributeDescriptors(): Map<String, AttributeDescriptor> {
),
"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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -143,6 +144,7 @@ class TreeSitterAdapterTest {
FileExtension.JAVA,
FileExtension.KOTLIN,
FileExtension.TYPESCRIPT,
FileExtension.TSX,
FileExtension.JAVASCRIPT,
FileExtension.PYTHON,
FileExtension.GO,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <div>Hello World</div>;
}
""".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 = (<h1>{x < 10 ? "Banana" : "Apple"}</h1>);"""
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 = (<div>{isLoggedIn && <Dashboard />}</div>);"""
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 (<Button onClick={() => doStuff()} />);
}
""".trimIndent()
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
}

@Test
fun `should count arrow function component assigned to const as a function`() {
// Arrange
val fileContent = """
const Fruit = () => (
<div>Hello</div>
);
""".trimIndent()
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
}

@Test
fun `should count logical OR short-circuit in JSX expression for complexity`() {
// Arrange
val fileContent = """const element = (<div>{error || <Content />}</div>);"""
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
}

@Test
fun `should accumulate complexity across multiple JSX conditional expressions`() {
// Arrange
val fileContent = """
function Card({ isAdmin, isActive, role }) {
return (
<div>
{isAdmin && <AdminBadge />}
{isActive ? <ActiveIcon /> : <InactiveIcon />}
{role === "guest" || <Content />}
</div>
);
}
""".trimIndent()
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
// 1 (function) + 1 (&&) + 1 (ternary) + 1 (||) = 4
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(4.0)
}

@Test
fun `should not count map callback returning JSX as a function`() {
// Arrange
val fileContent = """
function List({ items }) {
return (<ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>);
}
""".trimIndent()
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0)
}

@Test
fun `should count nullish coalescing operator in JSX expression for complexity`() {
// Arrange
val fileContent = """const element = (<div>{value ?? <Fallback />}</div>);"""
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(1.0)
}

@Test
fun `should count JSX block comment in comment lines`() {
// Arrange
val fileContent = """
function App() {
return (
<div>
{/* this is a JSX comment */}
<p>Hello</p>
</div>
);
}
""".trimIndent()
val input = createTestFile(fileContent)

// Act
val result = collector.collectMetricsForFile(input)

// Assert
assertThat(result.attributes[AvailableFileMetrics.COMMENT_LINES.metricName]).isEqualTo(1.0)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"checksum": "f983aebcae5f795440bf7a8ec6cadba1",
"data": {
"projectName": "",
"nodes": [
Expand Down Expand Up @@ -152,9 +151,11 @@
"description": "The mean complexity found in the body of a function of this file.",
"hintLowValue": "",
"hintHighValue": "",
"link": "",
"link": "https://codecharta.com/docs/parser/unified",
"direction": -1,
"analyzers": []
"analyzers": [
"unifiedParser"
]
},
"median_complexity_per_function": {
"title": "Median complexity per function",
Expand Down Expand Up @@ -268,5 +269,6 @@
}
},
"blacklist": []
}
},
"checksum": "cbe157f368d090f1a5105f10338e9ff7"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"checksum": "5b7a2f53493f8755370905f1a7c2d7a0",
"data": {
"projectName": "",
"nodes": [
Expand All @@ -19,7 +18,7 @@
"number_of_functions": 1.0,
"message_chains": 0.0,
"rloc": 5.0,
"loc": 8.0,
"loc": 9.0,
"long_method": 0.0,
"long_parameter_list": 0.0,
"excessive_comments": 0.0,
Expand Down Expand Up @@ -217,9 +216,11 @@
"description": "The mean complexity found in the body of a function of this file.",
"hintLowValue": "",
"hintHighValue": "",
"link": "",
"link": "https://codecharta.com/docs/parser/unified",
"direction": -1,
"analyzers": []
"analyzers": [
"unifiedParser"
]
},
"median_complexity_per_function": {
"title": "Median complexity per function",
Expand Down Expand Up @@ -333,5 +334,6 @@
}
},
"blacklist": []
}
},
"checksum": "8da2bef9265b511b8dedb9957779fd7d"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"checksum": "52c78daa83acfe0bedc2cc526b4ffa12",
"data": {
"projectName": "",
"nodes": [
Expand Down Expand Up @@ -163,7 +162,7 @@
"number_of_functions": 0.0,
"message_chains": 0.0,
"rloc": 9.0,
"loc": 14.0,
"loc": 15.0,
"long_method": 0.0,
"long_parameter_list": 0.0,
"excessive_comments": 0.0,
Expand Down Expand Up @@ -195,7 +194,7 @@
"number_of_functions": 1.0,
"message_chains": 0.0,
"rloc": 5.0,
"loc": 8.0,
"loc": 9.0,
"long_method": 0.0,
"long_parameter_list": 0.0,
"excessive_comments": 0.0,
Expand Down Expand Up @@ -393,9 +392,11 @@
"description": "The mean complexity found in the body of a function of this file.",
"hintLowValue": "",
"hintHighValue": "",
"link": "",
"link": "https://codecharta.com/docs/parser/unified",
"direction": -1,
"analyzers": []
"analyzers": [
"unifiedParser"
]
},
"median_complexity_per_function": {
"title": "Median complexity per function",
Expand Down Expand Up @@ -509,5 +510,6 @@
}
},
"blacklist": []
}
},
"checksum": "0b7ea0e9f7fd367f2b7d50f323962432"
}
Loading
Loading