Skip to content

Python: INFERRED uses edges anchor on the import line and bind to a co-located class instead of the importing function #2652

Description

@larya-dot-eu

Version: reproduced on 0.9.40 (and 0.9.17)

The AST extractor emits INFERRED uses edges that appear to encode "class C is defined in module M, M imports X, therefore C uses X". The endpoint is wrong whenever the importing module also defines a class: the function is the real user, not the class that happens to share the file. The edge is anchored at the import line, so source_location looks like corroboration.

Repro

helpers.py

class Helper:
    pass

api.py

from helpers import Helper


class Request:
    x: int = 0


def handler(req):
    return Helper()
from pathlib import Path
from graphify.extract import extract
r = extract([Path('api.py'), Path('helpers.py')], cache_root=Path('.'))
for e in r['edges']:
    if e.get('confidence') == 'INFERRED':
        print(e['source'], e['relation'], e['target'], e['source_file'], e['source_location'])

Actual

api_request --uses--> helpers_helper  [INFERRED]  api.py:L1

Expected — either api_handler --uses--> helpers_helper, or no inferred edge at all. Request never references Helper.

Why it matters

This is systematic rather than occasional in any framework codebase that co-locates request/response models with handlers. In a FastAPI + Pydantic repo (966 nodes) all 19 uses edges in the graph were false — not one was a genuine class-to-class relationship. Representative sample:

FieldGenerateRequest --uses--> Toggles          | from backend.prompts.router import PromptRouter, Toggles
LLMResult            --uses--> MockLLMClient    | from backend.llm.mock_client import MockLLMClient
LLMClient            --uses--> GeminiLLMClient  | from backend.llm.gemini_client import GeminiLLMClient

The first is fully spurious — that request model never touches Toggles. The others invert a real relationship: the clients return LLMResult and implement LLMClient, not the reverse.

These edges reach users: they surface in GRAPH_REPORT.md under "Surprising Connections", they are traversed by query, and the report's own "Are the N inferred relationships involving X actually correct?" prompt sends people to a source_location that shows only an import statement.

Workaround

Dropping AST-inferred edges whose source_location resolves to an import / from … import statement removed all 19 false positives here and kept all 15 genuine inferred edges (isinstance assertions, monkeypatch.setattr, constructor calls), so the import-line anchor cleanly separates the two classes.

Checked for duplicates

#2241 (call arguments binding to a single-letter test helper), #991 / #726 / #630 (cross-language identifier collapse in semantic chunks) are different mechanisms; #1633 is a feature request. This is structural extraction, single-language, and reproduces with no LLM backend involved.

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