Skip to content

indirect_call edges from module-top-level code keep absolute-path-derived source id (id_remap skips raw_calls) #2231

Description

@phhktb

Summary

Running the full extraction pipeline on a ~420-file JS/TS project, graphify.diagnostics.diagnose_extraction reports 586 dangling-endpoint edges. I traced the indirect_call subset (58 edges, all with a missing source) to a concrete bug in the id-remap pass in extract.py.

Confirmed bug: id_remap is applied to all_nodes/all_edges but never to all_raw_calls

  • extractors/engine.py:2342 creates a whole-file "module" node: file_nid = _make_id(str(path)), i.e. an id derived directly from the (often absolute) filesystem path.
  • That file_nid is used as scope_nid/caller_nid for indirect_call edges emitted from module-top-level code (calls not inside any function), e.g. via _emit_indirect_ref(ident, file_nid, js_module_bound, "collection") around engine.py:4468-4502. These go through raw_calls (all_raw_calls) for cross-file resolution.
  • extract.py:4661-4753 builds an id_remap dict that maps exactly this absolute-path-derived id (_make_id(str(path)) / _make_id(str(path.resolve()))) to the canonical relative form _file_node_id(rel) — this is the fix for graph.json edge endpoints still leak absolute-path-derived file ids #502.
  • extract.py:4754-4762 applies id_remap to all_nodes and all_edges only:
    if id_remap:
        for n in all_nodes:
            if n.get("id") in id_remap:
                n["id"] = id_remap[n["id"]]
        for e in all_edges:
            if e.get("source") in id_remap:
                e["source"] = id_remap[e["source"]]
            if e.get("target") in id_remap:
                e["target"] = id_remap[e["target"]]
    all_raw_calls is never touched by this loop.
  • The other remap (sym_remap, built from prefix_remap) does patch raw_calls[caller_nid] (extract.py:4820-4823), but sym_remap is keyed off _file_node_id(path) (the relative-form prefix), which never matches the absolute-form file_nid. So neither remap pass fixes raw_calls[caller_nid] for this id shape.
  • Net effect: when a raw_calls entry with caller_nid = file_nid (absolute-derived) is later resolved into a real indirect_call edge, its source keeps the stale absolute-path id — which matches no node in all_nodes (those were remapped), so it dangles.

Example (Windows, absolute input paths)

indirect_call: c_users_x_documents_github_myproj_src_index_js -> src_obfuscators_nonvm_obfuscatewithoutvm_obfuscatewithoutvm
  (source_file=src/index.js, source_location=L47)

Expected source id: src_index (matches the canonical file-node id used everywhere else).

Secondary observation (not root-caused, flagging for investigation)

Also saw ~50 dangling edges where an imports edge targets a barrel/re-export index.js file's re-exported symbol id instead of the file where the symbol is actually defined, e.g.:

imports: src_obfuscators_nonvm_nonvmbackenddefinition -> src_obfuscators_shared_backend_index_createbackendadapter

createBackendAdapter is defined in a sibling file under shared/backend/ and re-exported through index.js. There's existing re_exports/edge_alias_candidates repoint logic around extract.py:4824-4858 that looks like it's meant to handle exactly this shape, but I haven't traced why it doesn't catch this case — didn't want to guess, so flagging separately from the confirmed bug above.

Environment

  • graphify (graphifyy) installed via uv tool install
  • OS: Windows 11
  • Corpus: ~422 JS/TS files, extracted via graphify.extract.extract(code_files, cache_root=Path('.')) with absolute Path inputs from graphify.detect.detect()
  • Detected via: graphify.diagnostics.diagnose_extraction(extraction, directed=False, root='.')

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions