Skip to content

Python: a quoted forward-reference annotation (o: "Alpha") drops the call edge that the identical unquoted annotation resolves #1684

Closed

Description

@inth3shadows

Summary

Python: a parameter annotated with a string (forward-reference) annotation — def f(o: "Alpha") — loses receiver-type resolution that the identical unquoted annotation gets. The method call produces no calls edge at all.

Quoted annotations are ordinary Python (forward references, and everything under from __future__ import annotations), so this silently drops real edges in normal code.

Environment

@colbymchenry/codegraph 1.5.0 and 1.6.0 (npm latest), Linux, fresh codegraph init per version.

Repro

# pkg/a.py
def render(x):
    return x


class Alpha:
    def render(self):
        return "a"


class Beta:
    def render(self):
        return "b"
# pkg/b.py
from .a import Alpha


def quoted(o: "Alpha"):
    return o.render()      # -> NO EDGE

def bare(o: Alpha):
    return o.render()      # -> calls Alpha.render (pkg/a.py:6)

Observed — identical on 1.5.0 and 1.6.0

caller   target                    line
bare     render @pkg/a.py:6          9
(quoted produces no row at all)

Same file, same call expression, same class — only the quoting of the annotation differs.

Note on the trigger

The miss needs a competing same-named method to be present: with only Alpha.render (plus the top-level render) in the tree, the quoted form still resolved. Adding Beta.render is what makes the quoted form drop out while the bare form keeps resolving. So the quoted annotation appears not to establish the receiver type, leaving the call to name matching, which then declines under the extra candidate.

codegraph callers render correspondingly under-reports: quoted never appears.

Verification

Reproduced on both versions with a fresh index each time. Controls in the same run: the unquoted form resolves, and in a two-candidate variant of the same fixture the quoted form resolves — so this is the quoting interacting with candidate count, not a blanket annotation failure.

Activity

  1. inth3shadows commented on Sep 4, 2026

    @inth3shadows
    ContributorAuthor

    Additional evidence, replacing the speculative wording in my "Note on the trigger" above with what the index actually records.

    The call is extracted — it is not a missing ref. On the same fixture, unresolved_refs holds:

    from_node reference_name kind line candidates status
    quoted (pkg/b.py) o.render calls 5 (empty) failed

    So the pipeline reaches resolution with the receiver intact and then fails with zero candidates, while the unquoted bare on line 8 resolves to Alpha.render. That narrows it to receiver-type resolution not accepting a string annotation, rather than an extraction-side drop.

    Two consequences worth separating:

    1. The edge is lost — codegraph callers render reports only bare, never quoted.
    2. The loss is not visible to the caller. unresolved_refs.status='failed' records that the index knows this call could not be resolved, but the query surface returns an ordinary, unqualified result — indistinguishable from a symbol that genuinely has one caller. Anything reasoning about blast radius reads a confident answer over a known gap.

    Point 2 is arguably the more serious half and is not specific to this bug; happy to split it into its own issue if you would rather track it separately from the annotation handling.

    Verified on 1.5.0 and 1.6.0, fresh index each, cross-checked against both the CLI and direct SQL.

  2. colbymchenry commented on Sep 8, 2026

    @colbymchenry
    Owner

    Fixed in #1770.

    Reproduced on Linux against current main: with competing same-named methods (Alpha.render / Beta.render / top-level render), def quoted(o: "Alpha"): return o.render() produced no calls edge while the unquoted twin resolved to Alpha::render.

    Root cause: Python buildLocalReceiverTypePatterns only matched o: Alpha, so a quoted forward reference (o: "Alpha" / o: 'Alpha', including everything under from __future__ import annotations) contributed no receiver type and resolution failed with zero candidates.

    Fix (same approach as #1690, rebased surgically onto current main): admit the quoted annotation pattern before the unquoted one. Regression in __tests__/python-quoted-annotation.test.ts fails on main and passes with the fix; resolution.test.ts 198/198 green.

    Closing as completed. Thanks @inth3shadows for the tight repro and the unresolved_refs evidence.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions