Skip to content

Python AST extraction omits direct recursive calls while ordinary calls are emitted #3350

Description

@rebel-JuhwanKim

Summary

With graphifyy 0.9.54, direct Python recursion does not produce a calls self-edge in the output of graphify.extract.extract(). An ordinary call to the same function is extracted correctly.

This reproduces before graph construction, deduplication, export, or querying. The example invokes the upstream extractor directly: no custom adapter, semantic backend, or LLM is involved.

Minimal reproduction

Run this as a Python script in an environment with graphifyy==0.9.54 and its Python tree-sitter dependencies installed:

from pathlib import Path
from tempfile import TemporaryDirectory

from graphify.extract import extract

with TemporaryDirectory() as directory:
    root = Path(directory)
    source = root / "repro.py"
    source.write_text(
        "def factorial(n):\n"
        "    return 1 if n < 2 else n * factorial(n - 1)\n"
        "\n"
        "def entry(n):\n"
        "    return factorial(n)\n",
        encoding="utf-8",
    )

    result = extract([source], root=root, cache_root=root)
    calls = [
        (edge["source"], edge["target"])
        for edge in result["edges"]
        if edge.get("relation") == "calls"
    ]
    print("calls:", calls)
    print("ordinary call:", ("repro_entry", "repro_factorial") in calls)
    print("recursive call:", ("repro_factorial", "repro_factorial") in calls)

Actual result

calls: [('repro_entry', 'repro_factorial')]
ordinary call: True
recursive call: False

Both function nodes exist, but the call in the body of factorial is absent.

Expected result

In addition to entry -> factorial, include an extracted factorial -> factorial calls edge with the recursive call's source location. This would preserve the actual call structure for downstream inspection.

If omitting direct recursion is an intentional extractor contract, could that limitation be documented, or could retaining recursive calls be supported explicitly?

Source observations

The same condition is present on the current v8 branch at commit 937e59a5476fcb2665d6c4f4b7c0d0a4142011b6:

  • extractors/engine.py, direct-call handling: if tgt_nid and tgt_nid != caller_nid: excludes a resolved call whose target is its caller.
  • In the installed 0.9.54 extract.py, the common raw-call resolution path also requires tgt != caller (line 7154).
  • Conversely, the installed build.py explicitly preserves meaningful recursive calls self-edges while filtering only collapsed import/re-export self-loops. A supplied recursive edge survives graph construction.

This appears to be an extraction-stage omission, not loss of an existing edge during serialization or incremental merge. I have not established when the behavior was introduced, so this report does not claim a regression introduced specifically in 0.9.54.

Related reports checked

#2037 and #2038 address false import self-loops and preserving an already-supplied recursive calls edge in build_from_json(). The regression test in #2038 constructs that self-edge manually; it does not test extraction of a recursive Python function. The maintainer marked that issue fixed in 0.9.21.

#2374 / #2401 concern false self-loops caused by stub rewiring, rather than an absent direct recursive call.

I searched existing open/closed issues and PRs using recursion, recursive calls, self-loop, self-call, factorial, and caller_nid, and did not find a report for this specific extraction-stage case. Please link this to an existing report if I missed one.

Environment

  • Graphifyy: 0.9.54
  • Python: 3.13.14
  • Windows 11, build 26200, AMD64
  • tree-sitter: 0.25.2
  • tree-sitter-python: 0.25.0

The extractor and build modules used for the reproduction match the same-version upstream package files. Only a disposable synthetic source file and temporary extraction cache are used.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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