fix(python): bare class references produce references edges to classes (#1478) (#1493)

Python's class-as-value idioms (return SomeClass, x = SomeClass, registry
dicts, classes passed as arguments) produced no references edges, so
callers/impact on a Django/DRF serializer missed the views that consume it.
Three gates dropped them:

- return_statement was never dispatched by PYTHON_SPEC (kernel mirrored)
- the extraction gate (definedHere) collected function/method names only
- resolution accepted function/method targets only (matchFunctionRef +
  the function_ref import fast path)

Capture return_statement for Python (single expression; tuple returns not
descended), admit same-file CLASS names to the gate, and accept class
targets for Python bare identifiers — scoped to Python so the TS/JS KIND
FILTER contract is untouched. The docopt false-positive mechanism behind
the function-only rule (lowercase locals vs same-named methods) doesn't
transfer: methods stay excluded for bare ids, and the same-file/import
gate + unique-or-drop rules still apply.

Probed on django-rest-framework (~250 files): 559 new references→class
edges, 10/10 sampled genuine (serializer_class = AuthTokenSerializer, the
ModelSerializer field-mapping registry, aliases, ctor args, isinstance).
EXTRACTION_VERSION 24 → 25.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-08-01 01:36:07 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent f2a5df34de
commit 38580e0b04
9 changed files with 171 additions and 11 deletions
+1 -1
View File
@@ -21,4 +21,4 @@
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
* in the product is load-bearing").
*/
export const EXTRACTION_VERSION = 24;
export const EXTRACTION_VERSION = 25;
+6
View File
@@ -194,6 +194,12 @@ const PYTHON_SPEC: FnRefSpec = {
['keyword_argument', { mode: 'value', field: 'value' }], // Thread(target=worker)
['pair', { mode: 'value', field: 'value' }],
['list', { mode: 'list' }],
// `return SomeClass` / `return handler` — factory returns are how DRF
// wires views to serializers (get_serializer_class) and how Python
// factories hand back callables (#1478). A single returned expression is
// a direct named child, so 'list' covers it; tuple returns (`return A, B`)
// sit under an expression_list child and are deliberately not descended.
['return_statement', { mode: 'list' }],
]),
special: new Set(['attribute']),
};
+6
View File
@@ -649,6 +649,12 @@ export class TreeSitterExtractor {
const definedHere = new Set<string>();
for (const n of this.nodes) {
if (n.kind === 'function' || n.kind === 'method') definedHere.add(n.name);
// Python only (#1478): class-as-value is a first-class idiom (DRF
// get_serializer_class, Meta.model, registry dicts), so same-file CLASS
// names pass the gate too. Other languages keep the function/method
// gate — TS/JS recover class references through type annotations, and
// resolution's kind filter would drop their class candidates anyway.
else if (this.language === 'python' && n.kind === 'class') definedHere.add(n.name);
}
// Import-binding names only (all binding emitters push kind 'imports').
+9 -1
View File
@@ -915,7 +915,15 @@ export class ReferenceResolver {
const viaImport = this.gateLanguage(resolveViaImport(ref, this.context), ref);
if (viaImport) {
const target = this.queries.getNodeById(viaImport.targetNodeId);
if (target && (target.kind === 'function' || target.kind === 'method')) {
if (
target &&
(target.kind === 'function' ||
target.kind === 'method' ||
// Python (#1478): an imported class used as a value (`return
// OrgSerializerFull`) resolves through its import like any
// callback — mirrors matchFunctionRef's bareClassOk.
(ref.language === 'python' && target.kind === 'class'))
) {
return viaImport;
}
}
+13 -1
View File
@@ -232,6 +232,16 @@ export function matchFunctionRef(
ref.language === 'cpp' || ref.language === 'python' ||
ref.language === 'php';
// Python additionally accepts CLASS targets for bare identifiers (#1478):
// class-as-value is a core Python idiom (`return SomeSerializer`,
// `Meta.model = Org`, registry dicts, `admin.site.register(Model, Admin)`)
// and, unlike TS, Python has no type-annotation recovery path. The
// false-positive mechanism behind the function-only rule was lowercase
// locals colliding with same-named METHODS (docopt.py) — a candidate must
// be an exact-name CLASS node here, and the extraction gate (same-file
// class imports) plus unique-or-drop still apply. Methods stay excluded.
const bareClassOk = ref.language === 'python';
// Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
// resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m`
// shape is an explicit member reference). Unique-or-drop like everything else.
@@ -264,7 +274,9 @@ export function matchFunctionRef(
.getNodesByName(ref.referenceName)
.filter(
(n) =>
(n.kind === 'function' || (!bareFnOnly && n.kind === 'method')) &&
(n.kind === 'function' ||
(!bareFnOnly && n.kind === 'method') ||
(bareClassOk && n.kind === 'class')) &&
sameLanguageFamily(n.language, ref.language) &&
n.id !== ref.fromNodeId // a function registering itself is not a dependency edge
);