Skip to content

fix(python): resolve bare class references to their classes - #186

Merged
sunerpy merged 1 commit into
mainfrom
fix/python-bare-class-references
Aug 2, 2026
Merged

fix(python): resolve bare class references to their classes#186
sunerpy merged 1 commit into
mainfrom
fix/python-bare-class-references

Conversation

@sunerpy

@sunerpy sunerpy commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

Batch 4 of the upstream v1.5.0 sync, porting 38580e0 — the last portable item
of the round, and the only one that moves reference/golden/. Batches 1-3
shipped deliberately golden-neutral so this diff has exactly one possible cause.

Python treats a class as a first-class value, so a DRF view selects its
serializer with return OrgSerializerFull, a registry maps keys to classes, and
a factory hands one back. None of those produced an edge: callers on a
serializer showed nothing and impact reported a hollow radius, with the
consuming views invisible.

Three gates dropped these references independently, and all three had to move
together for one edge to survive:

  • The Python capture spec never dispatched return_statement.
  • The extraction gate admitted only functions and methods defined in the file,
    discarding a same-file class name before resolution.
  • Resolution accepted only function targets for a bare identifier, at both the
    name matcher and the import fast path.

Python now accepts class targets at both resolution sites through one shared
predicate
, so the two cannot drift apart. Methods stay excluded everywhere —
a bare name has no receiver, and the lowercase-local collisions that rule guards
against are unaffected by classes. Every other language keeps its existing kind
filter untouched. EXTRACTION_VERSION moves 2 → 3 so existing indexes
re-extract.

Tuple returns are a deliberate negative: return A, B puts a single
expression_list under the return, and list mode does not descend into it —
confirmed against tree-sitter-python-0.25.0's node-types.json, where
return_statement has no fields and children [expression, expression_list].

The golden fixture

The existing mini corpus could not serve: its only Python file constructs
(Greeter(...)) and calls (greeter.greet(...)), never naming a class as a
value, and its refs.json is empty. A dedicated
reference/golden/python/ fixture guards eight auditable shapes — six positives
(return, assignment RHS, registry pair, call argument, list literal, imported
class returned as a value) and two negatives (tuple return, bare method name).

The imported case resolves through the name matcher's unique-name path, not
the import fast path. find_exported_symbol requires an exported target and
Python never marks one — measured zero is_exported Python nodes across both
fixtures, and upstream is identical (python.ts implements no isExported,
import-resolver.ts:94 does if (!n.isExported) continue). So upstream's own
DRF assertion also comes from the name matcher. The import-side kind gate still
mirrors upstream, and its test states plainly that it hand-builds a state real
indexing cannot produce.

Verification

  • make ci green; cargo test --workspace 128 suites / 3127 passed / 0 failed
    (baseline v0.42.3 was 3121).
  • Each gate RED-proven by isolated revert: removing the return dispatch →
    names=[]; reverting the class gate → missing AliasClass: names=[]; stubbing
    the shared predicate → Python Class must be a function-ref target; reverting
    the version bump → left: 2, right: 3 and left: Building { built: 2 }, right: Outdated { built: 2 }.
  • Three-layer golden audit. Path set: exactly the six new
    reference/golden/python/* artifacts, zero drift in mini/godot/ruby/cpp
    (git diff cannot see new files and git status collapses untracked dirs, so
    the gate uses git ls-files --others for the new half and
    git diff --name-only for the tracked half). Attribution: exactly six
    class-target References edges, each mapping to one named shape —
    choose_return → ReturnClass (:1), choose_alias → AliasClass (:5),
    choose_registry → RegistryClass (:9), choose_argument → ArgumentClass (:13),
    choose_list → ListClass (:17) at 0.95 / function-ref, plus
    choose_imported → ImportedClass at 0.8; TupleA, TupleB and handler
    have zero incoming edges; refs.json holds exactly three unresolved rows.
    Reproducibility: regenerating into a fresh temp dir gave all five text
    artifacts byte-identical, and the 38-statement schema set matches every
    existing fixture.
  • Hands-on QA on a rebuilt binary reproduces the issue's own scenario:
    callers OrgSerializerFull now returns get_serializer_class at views.py:7,
    impact reports it among 3 affected symbols, both negatives hold, and a
    TypeScript return TsSerializer produces no function-ref edge — its only
    file→class edge remains imports / 0.9 / resolvedBy=import.

Plan approved after two momus rounds; gates F1-F4 all APPROVE (F2 after four,
each round closing a stale statement rather than a behaviour defect).

Python treats a class as a first-class value, so a DRF view selects its
serializer with `return OrgSerializerFull`, a registry maps keys to classes, and
a factory hands one back. None of those produced an edge, so `callers` on a
serializer showed nothing and `impact` reported a hollow radius: the views that
consume it were invisible.

Three gates each dropped these references independently, and all three had to
move together for one edge to survive:

- The Python capture spec never dispatched `return_statement`, so a returned
  bare name was not even collected. Tuple returns stay uncollected on purpose —
  `return A, B` puts a single `expression_list` under the return, and `list` mode
  does not descend into it.
- The extraction gate admitted only functions and methods defined in the file,
  so a same-file class name was discarded before resolution.
- Resolution accepted only function targets for a bare identifier, at both the
  name matcher and the import fast path.

Python now accepts class targets at both resolution sites through one shared
predicate, so the two cannot drift apart. Methods stay excluded everywhere: a
bare name has no receiver, and the lowercase-local collisions that rule guards
against are unaffected by classes. Every other language keeps its existing
kind filter untouched.

`EXTRACTION_VERSION` moves 2 → 3, so existing indexes classify as outdated and
re-extract to pick the new edges up.

A dedicated `reference/golden/python/` fixture guards the semantics: six
class-value shapes (return, assignment, registry pair, call argument, list
literal, and an imported class returned as a value) and two negatives (a tuple
return, and a bare method name). The existing mini corpus could not serve —
its only Python file constructs and calls, never naming a class as a value.

The imported case resolves through the name matcher's unique-name path, not the
import fast path: `find_exported_symbol` requires an exported target and Python
never marks one, upstream included. The import-side kind gate still mirrors
upstream, and its test says plainly that it hand-builds a state real indexing
cannot produce.
@sunerpy
sunerpy merged commit 3b33028 into main Aug 2, 2026
7 checks passed
@sunerpy
sunerpy deleted the fix/python-bare-class-references branch August 2, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant