fix(js): also shadow generator function-expression bindings; lock .tsx coupling (#2752)

Closes the gap #2752's title implies: a generator function EXPRESSION
(function*(k){}) parses as generator_function, which was in neither
_JS_DESCEND_TYPES nor the JS/TS function_boundary_types, so it still fabricated
an indirect_call to a same-named callable. Adds generator_function to both.
Adds a generator-FE regression test and a .tsx test that locks the
_TSX_CONFIG-by-reference coupling (untested before). Full JS/TS indirect_call
/calls regression re-run green (812 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-08-15 20:11:13 +01:00
co-authored by Claude Opus 4.8
parent 3512585ed0
commit c2a676d215
4 changed files with 40 additions and 3 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.9.44 (unreleased)
- Fix: a JS/TS inline or nested function expression — including a generator function expression (`function*(k){…}`) — no longer fabricates an INFERRED `indirect_call` when one of its parameters/locals shares a name with an unrelated callable; the expression's own bindings now shadow the name (#2752, thanks @imagineers-tyler), completing the shadow family alongside catch/arrow/loop/external-import (#2757).
- Fix: a git-tracked file that also matches a `.gitignore` pattern (a committed file later added to `.gitignore`, or a force-added one) is no longer dropped from the corpus, matching git's own behavior of never un-tracking such a file; `.graphifyignore`/`--exclude` stay authoritative and a non-git corpus is unaffected (#2759, thanks @NithishKumar04). The `git ls-files` probe is skipped entirely when no `.gitignore` is in play, so ordinary corpora pay nothing for it.
- Fix: doctest/Catch2 string-named test cases (`TEST_CASE("...")`, `SCENARIO`, `TEST_CASE_TEMPLATE`), which tree-sitter-cpp drops as ERROR nodes, are recovered as callable nodes contained by the file (#2594, thanks @ousamabenyounes); a punctuation-only test name gets a distinct line-positional id instead of collapsing onto the file-stem id.
+2 -2
View File
@@ -793,7 +793,7 @@ _JS_CONFIG = LanguageConfig(
# subtree and read as by-name references (#2241 family). A top-level
# `const f = function (…) {}` is tracked via its declarator and was already
# fine; the inline/nested forms are what this covers.
function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition", "function_expression"}),
function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition", "function_expression", "generator_function"}),
import_handler=_import_js,
)
@@ -815,7 +815,7 @@ _TS_CONFIG = LanguageConfig(
call_accessor_field="property",
call_accessor_object_field="object",
# `function_expression`: see the note on the JS config above.
function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition", "function_expression"}),
function_boundary_types=frozenset({"function_declaration", "generator_function_declaration", "arrow_function", "method_definition", "function_expression", "generator_function"}),
import_handler=_import_js,
)
+2 -1
View File
@@ -4711,7 +4711,8 @@ def _extract_generic(
# _tracked_body_ids guard below still prevents double-walking the
# top-level ones (those are entered via their own function_bodies entry).
_JS_DESCEND_TYPES = _JS_CLOSURE_TYPES + (
"function_declaration", "generator_function_declaration")
"function_declaration", "generator_function_declaration",
"generator_function")
def walk_calls(
node,
@@ -189,3 +189,38 @@ def test_typescript_function_expression_shadows(tmp_path):
os.chdir(old)
nid = {n["label"].rstrip("()"): n["id"] for n in r["nodes"]}
assert all(t != nid["k"] for _s, t in _indirect(r))
def test_generator_function_expression_param_shadows(tmp_path):
"""A GENERATOR function expression (`function*(k){}`) must shadow its param
the same way a plain function expression does — the type was in
generator_function_declaration form only, so the expression form still
fabricated the edge until generator_function joined the boundary set."""
r, nid = _extract_js_dir(tmp_path, {
"vendor.min.js": VENDOR,
"a.js": "export function run(xs, m){ const g = function*(k){ yield m.indexOf(k); }; return g; }\n",
})
assert all(t != nid["k"] for _s, t in _indirect(r))
def test_tsx_function_expression_shadows(tmp_path):
"""`.tsx` derives its boundary set from the TS config by reference; assert the
coupling so a future refactor that gives TSX its own literal can't silently
regress the shadow."""
base = tmp_path / "src"
base.mkdir()
(base / "vendor.min.tsx").write_text(VENDOR)
(base / "a.tsx").write_text(
"export function run(xs: number[], m: number[]) {\n"
" return xs.some(function (k: number) { return m.indexOf(k) >= 0; });\n"
"}\n"
)
old = os.getcwd()
try:
os.chdir(tmp_path)
r = extract([Path("src") / "vendor.min.tsx", Path("src") / "a.tsx"],
cache_root=Path(".cache"), parallel=False)
finally:
os.chdir(old)
nid = {n["label"].rstrip("()"): n["id"] for n in r["nodes"]}
assert all(t != nid["k"] for _s, t in _indirect(r))