From d825b26239ead77df87c35c4043eca5314599f5e Mon Sep 17 00:00:00 2001 From: nereboss Date: Tue, 11 Nov 2025 16:24:03 +0100 Subject: [PATCH 1/2] docs: add plan #4360 --- plans/plan_unifiedparser_tsx_jsx.md | 173 ++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 plans/plan_unifiedparser_tsx_jsx.md diff --git a/plans/plan_unifiedparser_tsx_jsx.md b/plans/plan_unifiedparser_tsx_jsx.md new file mode 100644 index 0000000000..c1ad973b33 --- /dev/null +++ b/plans/plan_unifiedparser_tsx_jsx.md @@ -0,0 +1,173 @@ +--- +name: Add TSX/JSX Language Support to UnifiedParser +issue: #4360 +state: todo +version: 1.0 +--- + +## Goal + +Add TSX (TypeScript with JSX) and JSX (JavaScript with JSX) language support to the UnifiedParser by utilizing the existing tree-sitter-typescript library which provides separate TSX and TypeScript grammars, creating TSX/JSX-specific node type mappings where needed, and ensuring all supported metrics are calculated correctly for .tsx and .jsx files. + +## Tasks + +### 1. Add TSX and JSX FileExtension Enums + +Add TSX and JSX entries to the FileExtension enum to recognize .tsx and .jsx files. + +- Edit `analysis/model/src/main/kotlin/de/maibornwolff/codecharta/serialization/FileExtension.kt` +- Add `TSX(".tsx")` entry following the pattern of other languages +- Add `JSX(".jsx")` entry following the pattern of other languages +- Place alphabetically (TSX after TYPESCRIPT, JSX after JAVASCRIPT) + +### 2. Create TsxNodeTypes Class + +Create a new TsxNodeTypes class that implements the MetricNodeTypes interface. This class defines the tree-sitter node types specific to TSX that correspond to each metric. TSX grammar extends TypeScript grammar with JSX elements. + +- Create `TsxNodeTypes.kt` in `analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/` +- Can initially inherit from TypescriptNodeTypes or reuse the same node type definitions +- Implement all required node type definitions (same as TypeScript initially): + - `logicComplexityNodeTypes`: if, loops, ternary, switch/case, catch, logical operators + - `functionComplexityNodeTypes`: functions, arrow functions, methods, generators + - `commentLineNodeTypes`: single-line comments, multi-line comments + - `numberOfFunctionsNodeTypes`: function declarations, method definitions + - `functionBodyNodeTypes`: statement blocks + - `functionParameterNodeTypes`: required parameters +- Research TSX's tree-sitter grammar to identify if any JSX-specific node types need special handling (e.g., JSX elements, JSX expressions) +- Note: JSX elements should generally not count as complexity unless they contain logic (conditional rendering, etc.) + +### 3. Create JsxNodeTypes Class + +Create a new JsxNodeTypes class that implements the MetricNodeTypes interface. JSX grammar extends JavaScript grammar with JSX elements. + +- Create `JsxNodeTypes.kt` in `analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metricnodetypes/` +- Can initially inherit from JavascriptNodeTypes or reuse similar definitions +- Follow the same pattern as TypeScript/TSX relationship +- Consider JSX-specific constructs similar to TSX + +### 4. Create TsxCollector Class + +Create a minimal TsxCollector class that extends MetricCollector and uses the TreeSitterTsx parser with TsxNodeTypes. + +- Create `TsxCollector.kt` in `analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/` +- Use TreeSitterTsx() as the tree-sitter language parser (separate from TreeSitterTypescript) +- Pass TsxNodeTypes() as the node type provider +- Follow the simple pattern from TypescriptCollector +- Note: The existing tree-sitter-typescript library already provides both TreeSitterTypescript and TreeSitterTsx classes + +### 5. Create JsxCollector Class + +Create a minimal JsxCollector class that extends MetricCollector and uses the TreeSitterJsx parser with JsxNodeTypes. + +- Create `JsxCollector.kt` in `analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/` +- Research if tree-sitter-javascript provides a separate JSX parser or if JavaScript parser handles JSX +- If separate parser exists: use TreeSitterJsx(), otherwise may need to reuse TreeSitterJavascript() +- Pass JsxNodeTypes() as the node type provider +- May need to investigate if a separate tree-sitter-jsx dependency is required + +### 6. Register TSX and JSX in AvailableCollectors + +Register the new TsxCollector and JsxCollector in the AvailableCollectors enum to make them accessible to the parser. + +- Edit `analysis/analysers/parsers/UnifiedParser/src/main/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/AvailableCollectors.kt` +- Add `TSX(FileExtension.TSX, ::TsxCollector)` entry +- Add `JSX(FileExtension.JSX, ::JsxCollector)` entry +- Place alphabetically + +### 7. Create Test Files and Tests + +Create comprehensive tests for the TsxCollector and JsxCollector following the TDD approach and existing test patterns. + +- Create test TSX sample file in `analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/tsxSample.tsx` with examples of: + - React components (function components, class components) + - JSX elements and fragments + - Conditional rendering (ternary, &&, etc.) + - TypeScript types and interfaces in TSX context + - Hooks and arrow functions + - Comments (single-line, multi-line, JSDoc) +- Create test JSX sample file in `analysis/analysers/parsers/UnifiedParser/src/test/resources/languageSamples/jsxSample.jsx` with similar React/JSX patterns +- Create `TsxCollectorTest.kt` in `analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/` +- Create `JsxCollectorTest.kt` in same directory +- Follow the Arrange-Act-Assert pattern with clear comments +- Test specific TSX/JSX constructs: + - JSX elements don't inflate complexity unnecessarily + - Conditional rendering (ternary in JSX) counts correctly + - Arrow functions in components are counted + - TypeScript types in TSX don't break parsing + - Comments within JSX are counted correctly +- Test all standard metrics: complexity, logic complexity, comment lines, functions, LOC, RLOC +- Test per-function metrics: parameters, complexity, RLOC (max/min/mean/median) + +### 8. Update Documentation + +Update the UnifiedParser README to include TSX and JSX in the supported languages table. + +- Edit `analysis/analysers/parsers/UnifiedParser/README.md` +- Add row to "Supported Languages" table: `| TSX | .tsx |` +- Add row to "Supported Languages" table: `| JSX | .jsx |` +- Place alphabetically +- Add note explaining that TSX/JSX use the same tree-sitter-typescript library but separate grammars + +### 9. Verify and Run Tests + +Run all tests to ensure TSX/JSX support works correctly and doesn't break existing functionality. + +- Run UnifiedParser tests: `cd analysis && ./gradlew :analysers:parsers:UnifiedParser:test` +- Run integration tests if applicable: `./gradlew integrationTest` +- Fix any failing tests or metric calculation issues +- If tree-sitter node types are incorrect, adjust TsxNodeTypes/JsxNodeTypes based on test results +- Ensure all new tests pass and existing tests remain green +- Verify TypeScript and JavaScript tests still pass (no regression) + +## Steps + +- [ ] Complete Task 1: Add TSX and JSX to FileExtension enum +- [ ] Complete Task 2: Create TsxNodeTypes class with JSX-aware node type definitions +- [ ] Complete Task 3: Create JsxNodeTypes class +- [ ] Complete Task 4: Create TsxCollector class using TreeSitterTsx +- [ ] Complete Task 5: Create JsxCollector class (research JSX parser availability first) +- [ ] Complete Task 6: Register TsxCollector and JsxCollector in AvailableCollectors enum +- [ ] Complete Task 7: Create test sample files and comprehensive TsxCollectorTest and JsxCollectorTest +- [ ] Complete Task 8: Update README.md documentation +- [ ] Complete Task 9: Run and verify all tests pass + +## Review Feedback Addressed + +(To be filled in after code review) + +## Notes + +- **Existing Dependency**: The tree-sitter-typescript library (version 0.23.2) is already included in the project and provides both `TreeSitterTypescript()` and `TreeSitterTsx()` parsers. No new dependency addition is required for TSX support. + +- **Separate Grammars**: TSX and TypeScript are treated as separate grammars in tree-sitter-typescript because TypeScript's type assertion syntax (`value`) conflicts with JSX element syntax (``). In TSX files, the type assertion syntax is disabled in favor of the `as` syntax (`value as Type`). + +- **JSX Parser Research Needed**: Need to verify if tree-sitter-javascript provides a separate JSX parser or if the JavaScript parser handles JSX natively. May need to check: + - Maven Central for tree-sitter-jsx or tree-sitter-javascript-jsx + - tree-sitter-javascript documentation + - Potentially use tree-sitter-typescript's TSX parser for JSX files as well + +- **TSX/JSX-Specific Considerations**: + - JSX elements (`
`, ``) should not inflate complexity metrics + - JSX expressions (`{variable}`, `{function()}`) should be counted normally + - Conditional rendering patterns (ternary, `&&`, `||` in JSX) should count as logic complexity + - Arrow functions used as component definitions should count as functions + - TypeScript type annotations in TSX should not break parsing + - Comments within JSX blocks (`{/* comment */}`) should be counted + +- **Testing Strategy**: Follow TDD (Red → Green → Refactor): + 1. Write failing test for specific TSX/JSX construct + 2. Implement minimum node type mapping to pass + 3. Run all tests + 4. Refactor if needed + 5. Commit when all tests pass + +- **Code Quality**: Follow project conventions: + - Use block-body style with braces for functions + - Add clear section comments in node type classes + - Use descriptive variable names + - No magic strings - extract constants if needed + - Ensure all warnings are resolved + +- **Node Type Reuse**: TsxNodeTypes can likely inherit from or delegate to TypescriptNodeTypes, and JsxNodeTypes can do the same with JavascriptNodeTypes. Only override if JSX-specific handling is needed. + +- **Future Enhancements**: If initial implementation doesn't handle all JSX constructs perfectly (e.g., JSX fragments, spread attributes), document known limitations in README.md. These can be addressed in future iterations. From d4103a5506cec708841d6cd711c8dfa7b7d16d07 Mon Sep 17 00:00:00 2001 From: nereboss Date: Thu, 13 Nov 2025 15:01:15 +0100 Subject: [PATCH 2/2] test: add tests for tsx #4360 Failing tests are marked with the fail reason as TODO --- .../metriccollectors/TsxCollectorTest.kt | 681 ++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt 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..080e921cef --- /dev/null +++ b/analysis/analysers/parsers/UnifiedParser/src/test/kotlin/de/maibornwolff/codecharta/analysers/parsers/unified/metriccollectors/TsxCollectorTest.kt @@ -0,0 +1,681 @@ +package de.maibornwolff.codecharta.analysers.parsers.unified.metriccollectors + +import de.maibornwolff.codecharta.analysers.parsers.unified.metricnodetypes.AvailableFileMetrics +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.treesitter.TSParser +import org.treesitter.TreeSitterTypescript +import java.io.File + +class TsxCollectorTest { + private var parser = TSParser() + private val collector = TypescriptCollector() + + @BeforeEach + fun setUp() { + parser.setLanguage(TreeSitterTypescript()) + } + + private fun createTestFile(content: String): File { + val tempFile = File.createTempFile("testFile", ".tsx") + tempFile.writeText(content) + tempFile.deleteOnExit() + return tempFile + } + + @Test + fun `should count functional component with JSX for number of functions`() { + // Arrange + val fileContent = """ + import React from 'react'; + + const MyComponent: React.FC = () => { + return
Hello World
; + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0) + } + + @Test + fun `should count function component with typed props for number of functions`() { + // Arrange + val fileContent = """ + interface Props { + name: string; + age: number; + } + + function UserCard({ name, age }: Props) { + return ( +
+

{name}

+

Age: {age}

+
+ ); + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(1.0) + } + + @Test + fun `should count class component with render method for number of functions`() { + // Arrange + val fileContent = """ + import React, { Component } from 'react'; + + interface State { + count: number; + } + + class Counter extends Component<{}, State> { + constructor(props: {}) { + super(props); + this.state = { count: 0 }; + } + + increment() { + this.setState({ count: this.state.count + 1 }); + } + + render() { + return ( +
+

Count: {this.state.count}

+ +
+ ); + } + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.NUMBER_OF_FUNCTIONS.metricName]).isEqualTo(3.0) + } + + @Test + fun `should count complexity with JSX ternary operators`() { + // Arrange + val fileContent = """ + const ConditionalComponent: React.FC<{ isLoading: boolean }> = ({ isLoading }) => { + return ( +
+ {isLoading ? : } +
+ ); + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(2.0) //TODO not counting ternary in div + } + + @Test + fun `should count complexity with JSX logical AND operators`() { + // Arrange + val fileContent = """ + const OptionalComponent = ({ showMessage }: { showMessage: boolean }) => { + return ( +
+ {showMessage &&

Message is visible

} + {!showMessage &&

Message is hidden

} +
+ ); + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(3.0) //TODO: correctly counts && even within errors + } + + @Test + fun `should count complexity with multiple conditional renders`() { + // Arrange + val fileContent = """ + const StatusDisplay = ({ status }: { status: string }) => { + if (status === 'loading') { + return ; + } else if (status === 'error') { + return ; + } else if (status === 'success') { + return ; + } + return ; + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(4.0) + } + + @Test + fun `should count complexity with switch statement for JSX rendering`() { + // Arrange + val fileContent = """ + const SwitchComponent = ({ type }: { type: string }) => { + switch (type) { + case 'primary': + return ; + case 'secondary': + return ; + case 'danger': + return ; + default: + return ; + } + } + """.trimIndent() + val input = createTestFile(fileContent) + + // Act + val result = collector.collectMetricsForFile(input) + + // Assert + Assertions.assertThat(result.attributes[AvailableFileMetrics.COMPLEXITY.metricName]).isEqualTo(5.0) //TODO: parsing of switch statements is incorrect after first < + } + + @Test + fun `should count JSX with fragments and multiple children`() { + // Arrange + val fileContent = """ + const FragmentComponent = () => { + return ( + <> +
+
+ +
+