mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 00:36:39 +00:00
The 0.9.37 #2551 warning fired on valid TypeScript/TSX: tree-sitter-typescript sets root.has_error on tiny fully-recovered errors (a `&` in a JSX string attribute; a semicolon-less `in_*` interface member) that still extract every symbol. The warning now fires only when recovery plausibly cost symbols — the file yielded <=1 node, or a materialized ERROR region spans more than one line — so the genuine Kotlin one-line-body (#2551) and Luau (#2520) cases still warn while valid TS goes silent. Warning also prints a root-relative path. Thanks @Sid-AutoWisdom and @atlasplatformu-ai. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e4d132b7da
commit
4b76ee127f
+16
-4
@@ -5471,12 +5471,24 @@ def extract(
|
||||
# naming the first error line so the user can find the construct.
|
||||
_syntax_error_files: list[tuple[str, int | None]] = []
|
||||
for i, _p in enumerate(paths):
|
||||
_pe = (per_file[i] or {}).get("parse_errors")
|
||||
if _pe:
|
||||
_syntax_error_files.append((str(_p), _pe.get("first_error_line")))
|
||||
_res = per_file[i] or {}
|
||||
_pe = _res.get("parse_errors")
|
||||
if not _pe:
|
||||
continue
|
||||
# #2610/#2599: gate the #2551 warning on plausible symbol loss.
|
||||
# tree-sitter-typescript sets has_error on tiny fully-recovered errors
|
||||
# (a `&` in a JSX string attr; a semicolon-less `in_*` interface
|
||||
# member) that extract completely — stay silent. Warn only when
|
||||
# nothing beyond the file node extracted, or an ERROR region
|
||||
# dissolved multiple lines (the genuine #2551 Kotlin one-line-body /
|
||||
# #2520 Luau case). `multiline_error` is absent from pre-fix cached
|
||||
# results, so those fall back to the file-node-only arm.
|
||||
if len(_res.get("nodes", [])) <= 1 or _pe.get("multiline_error"):
|
||||
_rel = os.path.relpath(str(_p), str(root)).replace("\\", "/")
|
||||
_syntax_error_files.append((_rel, _pe.get("first_error_line")))
|
||||
if _syntax_error_files:
|
||||
_shown = ", ".join(
|
||||
f"{Path(x).name} (first error at line {ln})" if ln else Path(x).name
|
||||
f"{x} (first error at line {ln})" if ln else x
|
||||
for x, ln in _syntax_error_files[:5]
|
||||
)
|
||||
_more = (
|
||||
|
||||
@@ -2344,6 +2344,19 @@ def _first_parse_error_line(root) -> int:
|
||||
node = child
|
||||
|
||||
|
||||
def _has_multiline_error(root) -> bool:
|
||||
"""True if any materialized ERROR node spans more than one line (a
|
||||
recovery region large enough to plausibly drop symbols, vs a tiny
|
||||
single-line recovery that extracts completely)."""
|
||||
stack = [root]
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
if n.type == "ERROR" and n.end_point[0] > n.start_point[0]:
|
||||
return True
|
||||
stack.extend(c for c in n.children if c.has_error)
|
||||
return False
|
||||
|
||||
|
||||
def _read_csharp_type_name(node, source: bytes) -> tuple[str, bool, str] | None:
|
||||
"""Resolve a C# type name, whether it was qualified, and its qualifier prefix."""
|
||||
if node is None:
|
||||
@@ -5255,7 +5268,10 @@ def _extract_generic(
|
||||
# error's line so extract() can warn instead of reporting silent success.
|
||||
# Rides on the result dict, so it survives the per-file AST cache.
|
||||
if root.has_error:
|
||||
result["parse_errors"] = {"first_error_line": _first_parse_error_line(root)}
|
||||
result["parse_errors"] = {
|
||||
"first_error_line": _first_parse_error_line(root),
|
||||
"multiline_error": _has_multiline_error(root),
|
||||
}
|
||||
# Kotlin (#2526/#2550): the declared package qualifies every node in the
|
||||
# file; the import-target and qualified-call resolvers key their per-package
|
||||
# symbol indexes off it.
|
||||
|
||||
@@ -15,9 +15,12 @@ older forks the extractor was written against:
|
||||
all-identifier chains into a `qualified_prefix` resolved against the
|
||||
declared packages (exactly-one-candidate guarded).
|
||||
* #2551 — the grammar rejects one-line `class C { val x }` bodies; consecutive
|
||||
one-liners can dissolve the whole file's parse. Graphify now warns on any
|
||||
file extracted through ERROR recovery (language-agnostic, also #2520) and
|
||||
keeps class linkage for declarations recovered inside an ERROR span.
|
||||
one-liners can dissolve the whole file's parse. Graphify warns on a file
|
||||
extracted through ERROR recovery (language-agnostic, also #2520) and keeps
|
||||
class linkage for declarations recovered inside an ERROR span. Since
|
||||
#2610/#2599 the warning fires only on PLAUSIBLE symbol loss (file-node-only
|
||||
result or a multiline ERROR region) — tiny fully-recovered errors that
|
||||
extract completely stay silent.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -272,7 +275,10 @@ def test_kotlin_partial_parse_warns_with_file_and_line(tmp_path, capsys):
|
||||
|
||||
def test_kotlin_one_line_class_with_fun_still_extracts(tmp_path, capsys):
|
||||
# `class VM { fun f() = 1 }` trips has_error but recovers structurally:
|
||||
# everything must extract, and the warning names the file.
|
||||
# everything must extract. #2610: since the recovery is zero-width and
|
||||
# every symbol is present, the corrected gate (warn only on plausible
|
||||
# symbol loss — file-node-only or a multiline ERROR region) stays SILENT;
|
||||
# the old has_error-gated warning here was a false positive.
|
||||
r = _extract(tmp_path, {
|
||||
"VM.kt": (
|
||||
"class VM { fun f() = 1 }\n"
|
||||
@@ -283,7 +289,7 @@ def test_kotlin_one_line_class_with_fun_still_extracts(tmp_path, capsys):
|
||||
f = _find(r, ".f()")
|
||||
_find(r, "After()") # present
|
||||
assert (vm, f) in _edges(r, "method")
|
||||
assert "VM.kt" in capsys.readouterr().err
|
||||
assert "VM.kt" not in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_kotlin_one_line_class_keeps_field_reference(tmp_path):
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""#2610/#2599: the #2551 partial-parse warning must not fire on VALID TS/TSX.
|
||||
|
||||
tree-sitter-typescript (0.23.x) sets ``has_error`` on tiny, fully-recovered
|
||||
errors in code ``tsc`` accepts — a ``&`` inside a JSX string attribute, or a
|
||||
semicolon-less interface member named ``in_*``/``instanceof_*`` — while still
|
||||
extracting every symbol. Gating the warning on bare ``has_error`` (0.9.37,
|
||||
#2551) therefore flagged valid files as "syntax errors". The corrected gate
|
||||
warns only on PLAUSIBLE symbol loss: nothing beyond the file node extracted,
|
||||
or an ERROR region spanning multiple lines.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _extract(tmp_path, files: dict[str, str]):
|
||||
for name, body in files.items():
|
||||
p = tmp_path / name
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body)
|
||||
old = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
r = extract([Path(n) for n in files],
|
||||
cache_root=tmp_path / ".cache", parallel=False)
|
||||
finally:
|
||||
os.chdir(old)
|
||||
return r
|
||||
|
||||
|
||||
def _labels(r):
|
||||
return {n["label"] for n in r["nodes"]}
|
||||
|
||||
|
||||
def _assert_silent(err):
|
||||
assert "syntax errors" not in err
|
||||
assert "partially extracted" not in err
|
||||
|
||||
|
||||
def test_tsx_amp_in_jsx_string_attr_is_silent(tmp_path, capsys):
|
||||
# `&` inside a JSX string attribute trips a grammar lexer bug: a 4-byte
|
||||
# ERROR node covering `&b=2`, has_error=True — but `el` extracts fine and
|
||||
# tsc accepts the file. No warning.
|
||||
r = _extract(tmp_path, {
|
||||
"app.tsx": (
|
||||
"declare const Comp: (props: { to: string }) => null;\n"
|
||||
'export const el = <Comp to="/p?a=1&b=2" />;\n'
|
||||
),
|
||||
})
|
||||
assert "el" in _labels(r)
|
||||
_assert_silent(capsys.readouterr().err)
|
||||
|
||||
|
||||
def test_ts_interface_member_named_in_prefix_is_silent(tmp_path, capsys):
|
||||
# A semicolon-less interface member starting with `in` (`in_workshop`)
|
||||
# yields a zero-width MISSING `}` + a 1-byte ERROR, has_error=True — but
|
||||
# `I` extracts fine and tsc accepts the file. No warning.
|
||||
r = _extract(tmp_path, {
|
||||
"types.ts": (
|
||||
"interface I { a: number\n"
|
||||
" in_workshop: number }\n"
|
||||
),
|
||||
})
|
||||
assert "I" in _labels(r)
|
||||
_assert_silent(capsys.readouterr().err)
|
||||
|
||||
|
||||
def test_ts_generics_as_and_jsx_logical_and_are_silent(tmp_path, capsys):
|
||||
# Regression canary: constructs superficially near the lexer bugs —
|
||||
# `foo<T>(x)`, an `as` cast, a `<T,>` arrow generic, `&&` in JSX — parse
|
||||
# CLEAN (no has_error) and must never warn.
|
||||
r = _extract(tmp_path, {
|
||||
"generics.ts": (
|
||||
"function foo<T>(x: T): T { return x }\n"
|
||||
"const y = foo<number>(1);\n"
|
||||
"const z = y as number;\n"
|
||||
),
|
||||
"arrow.tsx": (
|
||||
"const pick = <T,>(x: T) => x;\n"
|
||||
"export const view = <div>{true && <span>hi</span>}</div>;\n"
|
||||
),
|
||||
})
|
||||
assert {"foo()", "pick()", "view"} <= _labels(r)
|
||||
_assert_silent(capsys.readouterr().err)
|
||||
|
||||
|
||||
def test_ts_genuinely_broken_file_still_warns(tmp_path, capsys):
|
||||
# `function f( {` dissolves the whole parse — nothing beyond the file
|
||||
# node extracts. The warning must fire and name the file + first line.
|
||||
_extract(tmp_path, {"broken.ts": "function f( {\n"})
|
||||
err = capsys.readouterr().err
|
||||
assert "syntax errors" in err
|
||||
assert "broken.ts" in err
|
||||
assert re.search(r"first error at line 1", err)
|
||||
|
||||
|
||||
def test_ts_midfile_breakage_warns_and_keeps_intact_functions(tmp_path, capsys):
|
||||
# An unclosed brace between two valid functions produces a multiline
|
||||
# ERROR region: the warning fires AND both intact functions extract.
|
||||
r = _extract(tmp_path, {
|
||||
"midfile.ts": (
|
||||
"function alpha() { return 1 }\n"
|
||||
"\n"
|
||||
"function bad( {\n"
|
||||
" oops;\n"
|
||||
" more;\n"
|
||||
"\n"
|
||||
"function beta() { return 2 }\n"
|
||||
),
|
||||
})
|
||||
labels = _labels(r)
|
||||
assert "alpha()" in labels
|
||||
assert "beta()" in labels
|
||||
err = capsys.readouterr().err
|
||||
assert "syntax errors" in err
|
||||
assert "midfile.ts" in err
|
||||
Reference in New Issue
Block a user