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:
co-authored by
Serkan Gezici
parent
3e1c257dde
commit
8489b26d06
+24
-1
@@ -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)
|
||||
|
||||
|
||||
|
||||
Vendored
+18
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user