Drop edges of a JS node dropped for colliding with a PHP node

A reviewer on this PR pointed out that when a JS symbol's id collides
with an existing PHP node (same name, one file, resolved in favor of
the PHP node), the merge still appended every JS edge unconditionally.
An edge meant for the discarded JS symbol silently attached to the
unrelated PHP node sharing its id instead of vanishing with it. The
file node's own always on collision (both passes deliberately reuse
its id) is excluded from this so an unrelated JS symbol's own edges
sourced from the file node are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit c96169b320)
This commit is contained in:
ayushcodes10 authored and safishamsi committed 2026-09-18 15:15:58 +01:00
1 parent cc80351872
commit 96e653cc7c
1 file changed
+23 -4
+23 -4
View File
@@ -2593,7 +2593,9 @@ def extract_php(path: Path) -> dict:
file node id, so JS symbols land under the one file node like any other
PHP symbol; a same-name PHP/JS collision (rare — different languages,
same file) is resolved by keeping the PHP node, since PHP is the file's
primary language.
primary language, and dropping every JS edge that touches the id the
dropped JS node would have used, so no edge from the discarded symbol
is left to silently attach to the unrelated PHP node sharing its id.
"""
result = _extract_generic(path, _PHP_CONFIG)
try:
@@ -2602,12 +2604,29 @@ def extract_php(path: Path) -> dict:
if not had_script:
return result
js_result = _extract_generic(path, _JS_CONFIG, source_override=masked.encode("utf-8"))
# The file node itself always collides by construction -- both passes
# derive its id from the same path, on purpose, so JS symbols can
# land under the one PHP file node. That expected collision must not
# be treated as a dropped-symbol collision below, or every JS edge
# sourced from the file node (i.e. every top-level JS symbol's own
# `contains` edge) would be discarded along with it.
file_node_id = result["nodes"][0]["id"] if result.get("nodes") else None
existing_ids = {n["id"] for n in result.get("nodes", [])}
colliding_ids: set = set()
for n in js_result.get("nodes", []):
if n["id"] not in existing_ids:
result.setdefault("nodes", []).append(n)
existing_ids.add(n["id"])
if n["id"] in existing_ids:
if n["id"] != file_node_id:
colliding_ids.add(n["id"])
continue
result.setdefault("nodes", []).append(n)
existing_ids.add(n["id"])
for e in js_result.get("edges", []):
# A JS node dropped above for colliding with an existing PHP
# node id must not leave its edges behind -- otherwise an edge
# meant for the dropped JS symbol silently attaches to the
# unrelated retained PHP node that happens to share its id.
if e.get("source") in colliding_ids or e.get("target") in colliding_ids:
continue
result.setdefault("edges", []).append(e)
except Exception:
pass