Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
71b4e57b67
commit
4dce16f475
@@ -178,6 +178,23 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
||||
attrs = {k: v for k, v in edge.items() if k not in ("source", "target")}
|
||||
if "source_file" in attrs:
|
||||
attrs["source_file"] = _norm_source_file(attrs["source_file"], _root)
|
||||
# Drop cross-language INFERRED `calls` edges — same short names (render,
|
||||
# parse, etc.) appear across language boundaries in multi-language chunks,
|
||||
# producing phantom edges that don't represent real call relationships.
|
||||
if attrs.get("relation") == "calls" and attrs.get("confidence") == "INFERRED":
|
||||
_LANG_FAMILY: dict[str, str] = {
|
||||
".py": "py", ".pyi": "py",
|
||||
".js": "js", ".mjs": "js", ".cjs": "js", ".jsx": "js",
|
||||
".ts": "js", ".tsx": "js",
|
||||
".go": "go", ".rs": "rs",
|
||||
".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm",
|
||||
".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp",
|
||||
".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
|
||||
}
|
||||
src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower()
|
||||
tgt_ext = Path(G.nodes[tgt].get("source_file") or "").suffix.lower()
|
||||
if src_ext and tgt_ext and _LANG_FAMILY.get(src_ext) != _LANG_FAMILY.get(tgt_ext):
|
||||
continue
|
||||
# Preserve original edge direction - undirected graphs lose it otherwise,
|
||||
# causing display functions to show edges backwards.
|
||||
attrs["_src"] = src
|
||||
|
||||
+19
-17
@@ -1745,11 +1745,13 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
walk(root)
|
||||
|
||||
# ── Call-graph pass ───────────────────────────────────────────────────────
|
||||
label_to_nid: dict[str, str] = {}
|
||||
label_to_nid: dict[str, str] = {} # case-sensitive (Ruby, C#, Java, Kotlin, etc.)
|
||||
label_to_nid_ci: dict[str, str] = {} # case-insensitive (PHP functions/classes)
|
||||
for n in nodes:
|
||||
raw = n["label"]
|
||||
normalised = raw.strip("()").lstrip(".")
|
||||
label_to_nid[normalised.lower()] = n["id"]
|
||||
label_to_nid[normalised] = n["id"]
|
||||
label_to_nid_ci[normalised.lower()] = n["id"]
|
||||
|
||||
seen_call_pairs: set[tuple[str, str]] = set()
|
||||
seen_dyn_import_pairs: set[tuple[str, str]] = set()
|
||||
@@ -1892,7 +1894,7 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
callee_name = _read_text(func_node, source)
|
||||
|
||||
if callee_name:
|
||||
tgt_nid = label_to_nid.get(callee_name.lower())
|
||||
tgt_nid = label_to_nid.get(callee_name)
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_call_pairs:
|
||||
@@ -1937,8 +1939,8 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
break
|
||||
if first_key:
|
||||
segment = first_key.split(".")[0]
|
||||
tgt_nid = (label_to_nid.get(segment.lower())
|
||||
or label_to_nid.get(f"{segment}.php".lower()))
|
||||
tgt_nid = (label_to_nid_ci.get(segment.lower())
|
||||
or label_to_nid_ci.get(f"{segment}.php".lower()))
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
relation = f"uses_{callee_name}"
|
||||
pair3 = (caller_nid, tgt_nid, relation)
|
||||
@@ -1976,8 +1978,8 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
break
|
||||
if len(class_args) == 2:
|
||||
contract_name, impl_name = class_args
|
||||
contract_nid = label_to_nid.get(contract_name.lower())
|
||||
impl_nid = label_to_nid.get(impl_name.lower())
|
||||
contract_nid = label_to_nid_ci.get(contract_name.lower())
|
||||
impl_nid = label_to_nid_ci.get(impl_name.lower())
|
||||
if contract_nid and impl_nid and contract_nid != impl_nid:
|
||||
pair3 = (contract_nid, impl_nid, "bound_to")
|
||||
if pair3 not in seen_bind_pairs:
|
||||
@@ -2004,7 +2006,7 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
break
|
||||
if scope_node is not None:
|
||||
class_name = _read_text(scope_node, source)
|
||||
tgt_nid = label_to_nid.get(class_name.lower())
|
||||
tgt_nid = label_to_nid_ci.get(class_name.lower())
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair3 = (caller_nid, tgt_nid, "uses_static_prop")
|
||||
if pair3 not in seen_static_ref_pairs:
|
||||
@@ -2025,7 +2027,7 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
if config.ts_module == "tree_sitter_php" and node.type == "class_constant_access_expression":
|
||||
class_name = _php_class_const_scope(node)
|
||||
if class_name:
|
||||
tgt_nid = label_to_nid.get(class_name.lower())
|
||||
tgt_nid = label_to_nid_ci.get(class_name.lower())
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair3 = (caller_nid, tgt_nid, "references_constant")
|
||||
if pair3 not in seen_static_ref_pairs:
|
||||
@@ -2051,8 +2053,8 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
# ── Event listener pass ───────────────────────────────────────────────────
|
||||
seen_listen_pairs: set[tuple[str, str]] = set()
|
||||
for event_name, listener_name, line in pending_listen_edges:
|
||||
event_nid = label_to_nid.get(event_name.lower())
|
||||
listener_nid = label_to_nid.get(listener_name.lower())
|
||||
event_nid = label_to_nid_ci.get(event_name.lower())
|
||||
listener_nid = label_to_nid_ci.get(listener_name.lower())
|
||||
if not event_nid or not listener_nid or event_nid == listener_nid:
|
||||
continue
|
||||
pair2 = (event_nid, listener_nid)
|
||||
@@ -3716,7 +3718,7 @@ def extract_go(path: Path) -> dict:
|
||||
for n in nodes:
|
||||
raw = n["label"]
|
||||
normalised = raw.strip("()").lstrip(".")
|
||||
label_to_nid[normalised.lower()] = n["id"]
|
||||
label_to_nid[normalised] = n["id"]
|
||||
|
||||
seen_call_pairs: set[tuple[str, str]] = set()
|
||||
raw_calls: list[dict] = []
|
||||
@@ -3741,7 +3743,7 @@ def extract_go(path: Path) -> dict:
|
||||
if field:
|
||||
callee_name = _read_text(field, source)
|
||||
if callee_name:
|
||||
tgt_nid = label_to_nid.get(callee_name.lower())
|
||||
tgt_nid = label_to_nid.get(callee_name)
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_call_pairs:
|
||||
@@ -3912,7 +3914,7 @@ def extract_rust(path: Path) -> dict:
|
||||
for n in nodes:
|
||||
raw = n["label"]
|
||||
normalised = raw.strip("()").lstrip(".")
|
||||
label_to_nid[normalised.lower()] = n["id"]
|
||||
label_to_nid[normalised] = n["id"]
|
||||
|
||||
seen_call_pairs: set[tuple[str, str]] = set()
|
||||
raw_calls: list[dict] = []
|
||||
@@ -3942,7 +3944,7 @@ def extract_rust(path: Path) -> dict:
|
||||
if name:
|
||||
callee_name = _read_text(name, source)
|
||||
if callee_name:
|
||||
tgt_nid = label_to_nid.get(callee_name.lower())
|
||||
tgt_nid = label_to_nid.get(callee_name)
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_call_pairs:
|
||||
@@ -5839,7 +5841,7 @@ def extract_elixir(path: Path) -> dict:
|
||||
label_to_nid: dict[str, str] = {}
|
||||
for n in nodes:
|
||||
normalised = n["label"].strip("()").lstrip(".")
|
||||
label_to_nid[normalised.lower()] = n["id"]
|
||||
label_to_nid[normalised] = n["id"]
|
||||
|
||||
seen_call_pairs: set[tuple[str, str]] = set()
|
||||
raw_calls: list[dict] = []
|
||||
@@ -5877,7 +5879,7 @@ def extract_elixir(path: Path) -> dict:
|
||||
callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
||||
break
|
||||
if callee_name:
|
||||
tgt_nid = label_to_nid.get(callee_name.lower())
|
||||
tgt_nid = label_to_nid.get(callee_name)
|
||||
if tgt_nid and tgt_nid != caller_nid:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_call_pairs:
|
||||
|
||||
+1
-1
@@ -346,7 +346,7 @@ Rules:
|
||||
Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns).
|
||||
Do not re-extract imports - AST already has those.
|
||||
Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made, trade-offs, design intent): store as a `rationale` attribute on the relevant concept node — do NOT create a separate rationale node or fragment node. Only create a node for something that is itself a named entity or concept. Use `file_type:"rationale"` for concept-like nodes (ideas, principles, mechanisms, design patterns). `file_type` MUST be one of exactly these six values: `code`, `document`, `paper`, `image`, `rationale`, `concept`. Any other value is invalid and will be rejected.
|
||||
Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling), target MUST be the callee. Never reverse this direction.
|
||||
Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling), target MUST be the callee. Never reverse this direction. `calls` edges MUST stay within one language: a Python function cannot `calls` a JS/TS/Go/Rust/Java symbol and vice versa — cross-language call edges are phantom artifacts, never emit them.
|
||||
Image files: use vision to understand what the image IS - do not just OCR.
|
||||
UI screenshot: layout patterns, design decisions, key elements, purpose.
|
||||
Chart: metric, trend/insight, data source.
|
||||
|
||||
Reference in New Issue
Block a user