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:
co-authored by
Claude Fable 5
parent
f2a5df34de
commit
38580e0b04
@@ -11,7 +11,9 @@
|
||||
* - decoy: an ambiguous cross-file name (no import, ≥2 definitions) → NO edge
|
||||
* - same-file priority: a same-file definition beats a same-named decoy
|
||||
* - kind filter: a class/variable passed as a value never gets a
|
||||
* function-ref edge
|
||||
* function-ref edge — except Python, where class-as-value is a core
|
||||
* idiom and bare ids ALSO resolve to classes (#1478); methods stay
|
||||
* excluded for bare ids everywhere
|
||||
* - self: a function passing itself → no self-loop
|
||||
* - drain: all resolvable function_ref rows leave unresolved_refs (no
|
||||
* batched-resolver runaway), and re-index is idempotent
|
||||
@@ -744,6 +746,117 @@ describe('Function-as-value capture (#756)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('PYTHON CLASSES: return / alias / registry dict / arg positions produce references edges (#1478)', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-pycls-'));
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'serializers.py'),
|
||||
[
|
||||
'class OrgSerializerFull:',
|
||||
' pass',
|
||||
'',
|
||||
'class OrgSerializerBrief:',
|
||||
' pass',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'views.py'),
|
||||
[
|
||||
'from serializers import OrgSerializerFull, OrgSerializerBrief',
|
||||
'',
|
||||
'def register(cls):',
|
||||
' pass',
|
||||
'',
|
||||
'class OrgViewSet:',
|
||||
' def get_serializer_class(self):',
|
||||
' if True:',
|
||||
' return OrgSerializerFull',
|
||||
' return OrgSerializerBrief',
|
||||
'',
|
||||
'SERIALIZER_REGISTRY = {"org": OrgSerializerFull}',
|
||||
'register(OrgSerializerBrief)',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'models.py'),
|
||||
[
|
||||
'class Config:',
|
||||
' pass',
|
||||
'',
|
||||
'def make_config_cls():',
|
||||
' return Config',
|
||||
'',
|
||||
'ActiveConfig = Config',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
try {
|
||||
await cg.indexAll();
|
||||
|
||||
// The DRF wiring: get_serializer_class → the imported serializer class,
|
||||
// via `return` — the issue's headline gap. The module-level registry
|
||||
// dict rides the file node.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerFull'))).toEqual([
|
||||
'get_serializer_class',
|
||||
'views.py',
|
||||
]);
|
||||
// Second branch return + a module-level call argument.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'OrgSerializerBrief'))).toEqual([
|
||||
'get_serializer_class',
|
||||
'views.py',
|
||||
]);
|
||||
|
||||
// Same-file: factory return + module-level alias assignment.
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'Config'))).toEqual([
|
||||
'make_config_cls',
|
||||
'models.py',
|
||||
]);
|
||||
|
||||
// callers() must now surface the view as a consumer of the serializer.
|
||||
const serializer = cg
|
||||
.getNodesByName('OrgSerializerFull')
|
||||
.find((n) => n.kind === 'class')!;
|
||||
const callers = cg.getCallers(serializer.id);
|
||||
expect(callers.some((c) => c.node.name === 'get_serializer_class')).toBe(true);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('PYTHON KIND FILTER: bare ids still never resolve to methods; unknown names stay silent', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-pyneg-'));
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'svc.py'),
|
||||
[
|
||||
'class Svc:',
|
||||
' def refresh(self):',
|
||||
' pass',
|
||||
'',
|
||||
'def wire(cb):',
|
||||
' pass',
|
||||
'',
|
||||
'def setup(refresh):',
|
||||
// A local/parameter sharing a same-file METHOD name: the gate lets it
|
||||
// through (methods are in definedHere) but resolution must refuse —
|
||||
// a bare id can never be a method value in Python.
|
||||
' wire(refresh)',
|
||||
// A name with no matching class/function anywhere: no edge, silently.
|
||||
' return unknown_thing',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const cg = CodeGraph.initSync(tmpDir);
|
||||
try {
|
||||
await cg.indexAll();
|
||||
expect(fnRefEdgesInto(cg, 'refresh')).toHaveLength(0);
|
||||
expect(fnRefEdgesInto(cg, 'unknown_thing')).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('DRAIN: resolvable function_ref rows leave unresolved_refs; re-index is stable', async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-drain-'));
|
||||
fs.writeFileSync(
|
||||
|
||||
Reference in New Issue
Block a user