diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bb228d..d9d27742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/graphify/extract.py b/graphify/extract.py index f032a025..396aa265 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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, ) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 4dad535d..61b5995f 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -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, diff --git a/tests/test_indirect_call_function_expression_shadow.py b/tests/test_indirect_call_function_expression_shadow.py index e07507d1..10db4976 100644 --- a/tests/test_indirect_call_function_expression_shadow.py +++ b/tests/test_indirect_call_function_expression_shadow.py @@ -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))