fix(extract): use language_tsx for .tsx files to enable JSX-aware parsing (#766)

tree-sitter-typescript ships two grammars:
- language_typescript: pure TypeScript, no JSX support
- language_tsx:        JSX-aware variant for .tsx files

Currently both .ts and .tsx are parsed with language_typescript, which
treats JSX syntax as parse errors. Every function declaration, arrow
function, and call_expression nested inside a JSX tree is silently
dropped from the extracted graph.

Repro on a representative React+TypeScript codebase (a 13-file Tauri app):
parsing each .tsx with language_typescript produces ~276 ERROR nodes per
file. Only declarations that happen to live before the first JSX block
survive.

Fix: add _TSX_CONFIG that mirrors _TS_CONFIG but selects language_tsx,
and route .tsx files to it in extract_js().

Effect on the same repo (graphify update --force):
  Nodes:        303 → 618  (+104%)
  Edges:        482 → 779  (+62%)
  Communities:   28 → 45   (+61%)
  Parse errors  276 → 0    per .tsx file

Tests added:
- tsx fixture with helpers + JSX-returning component
- helpers and component are captured
- JSX expression calls ({fmtDate(now)}) resolve to call edges
- wiring check: .tsx uses language_tsx, .ts uses language_typescript

Note: this fixes the parsing layer. Calls inside deeply nested arrow
function callbacks (e.g. items.map(x => <T>{f(x)}</T>)) are still
missed by the call extraction logic — separate enhancement.

Co-authored-by: Serkan Gezici <serkan@quadroaipilot.com>
This commit is contained in:
SerkanGezici
2026-05-07 20:07:36 +01:00
committed by GitHub
co-authored by Serkan Gezici
parent 3e1c257dde
commit 8489b26d06
3 changed files with 86 additions and 1 deletions
+24 -1
View File
@@ -911,6 +911,24 @@ _TS_CONFIG = LanguageConfig(
import_handler=_import_js,
)
# .tsx files must use the TSX grammar (JSX-aware), not the plain TypeScript grammar.
# tree-sitter-typescript ships two languages: language_typescript (for .ts) and
# language_tsx (for .tsx). Parsing .tsx with language_typescript silently fails on
# JSX expressions, dropping any call_expression nested inside JSX (e.g. {fmtDate(x)}).
_TSX_CONFIG = LanguageConfig(
ts_module="tree_sitter_typescript",
ts_language_fn="language_tsx",
class_types=_TS_CONFIG.class_types,
function_types=_TS_CONFIG.function_types,
import_types=_TS_CONFIG.import_types,
call_types=_TS_CONFIG.call_types,
call_function_field=_TS_CONFIG.call_function_field,
call_accessor_node_types=_TS_CONFIG.call_accessor_node_types,
call_accessor_field=_TS_CONFIG.call_accessor_field,
function_boundary_types=_TS_CONFIG.function_boundary_types,
import_handler=_TS_CONFIG.import_handler,
)
_JAVA_CONFIG = LanguageConfig(
ts_module="tree_sitter_java",
class_types=frozenset({"class_declaration", "interface_declaration"}),
@@ -1951,7 +1969,12 @@ def extract_python(path: Path) -> dict:
def extract_js(path: Path) -> dict:
"""Extract classes, functions, arrow functions, and imports from a .js/.ts/.tsx file."""
config = _TS_CONFIG if path.suffix in (".ts", ".tsx") else _JS_CONFIG
if path.suffix == ".tsx":
config = _TSX_CONFIG
elif path.suffix == ".ts":
config = _TS_CONFIG
else:
config = _JS_CONFIG
return _extract_generic(path, config)
+18
View File
@@ -0,0 +1,18 @@
function fmtDate(d: Date): string {
return d.toISOString();
}
function fmtCount(n: number): string {
return `${n} items`;
}
export function App() {
const now = new Date();
return (
<div className="app">
<h1>Header</h1>
<span>{fmtDate(now)}</span>
<span>{fmtCount(42)}</span>
</div>
);
}
+44
View File
@@ -321,3 +321,47 @@ def test_cross_file_call_remains_inferred_without_import_evidence(tmp_path):
]
assert len(call_edges) == 1
assert call_edges[0]["confidence"] == "INFERRED"
# ── TSX (JSX-aware) parsing ──────────────────────────────────────────────────
# .tsx files require tree-sitter-typescript's `language_tsx`, not the plain
# `language_typescript` grammar. Parsing JSX with the wrong grammar produces
# silent ERROR nodes and drops every function/call inside JSX trees.
def test_extract_tsx_finds_helpers_and_component():
"""Functions defined alongside a JSX-returning component must be captured."""
from graphify.extract import extract_js
result = extract_js(FIXTURES / "sample.tsx")
labels = [n["label"] for n in result["nodes"]]
assert any("fmtDate" in l for l in labels), f"fmtDate missing from {labels}"
assert any("fmtCount" in l for l in labels), f"fmtCount missing from {labels}"
assert any("App" in l for l in labels), f"App missing from {labels}"
def test_extract_tsx_jsx_expression_calls_resolve():
"""Calls inside JSX expressions like `{fmtDate(now)}` must yield call edges.
Regression guard for the TSX language fix: with `language_typescript`,
JSX is parsed as ERROR nodes and these call_expressions disappear.
"""
from graphify.extract import extract_js
result = extract_js(FIXTURES / "sample.tsx")
nodes_by_id = {n["id"]: n for n in result["nodes"]}
call_targets = {
nodes_by_id[e["target"]]["label"]
for e in result["edges"]
if e["relation"] == "calls" and e["target"] in nodes_by_id
}
assert "fmtDate()" in call_targets, (
f"JSX expression call to fmtDate() not captured. Targets: {call_targets}"
)
assert "fmtCount()" in call_targets, (
f"JSX expression call to fmtCount() not captured. Targets: {call_targets}"
)
def test_extract_tsx_uses_tsx_grammar():
"""Wiring check: the .tsx config must use tree-sitter's `language_tsx`."""
from graphify.extract import _TSX_CONFIG, _TS_CONFIG
assert _TSX_CONFIG.ts_language_fn == "language_tsx"
assert _TS_CONFIG.ts_language_fn == "language_typescript"