fix(resolution): read a quoted Python annotation as a receiver type (#1684) (#1770)

`def f(o: "Alpha")` is the same annotation as `def f(o: Alpha)` — a
forward reference, and what every file under `from __future__ import
annotations` writes — but the receiver-type pattern stopped at the quote,
read no type, and `o.render()` produced no edge. Admit the quoted form.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 09:30:01 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 4105249843
commit 3adf06772b
3 changed files with 62 additions and 0 deletions
@@ -0,0 +1,55 @@
/**
* A quoted (forward-reference) parameter annotation names a receiver type too
* (#1684): `def f(o: "Alpha")` resolves `o.render()` exactly like `def f(o:
* Alpha)`. Quoted annotations are ordinary Python — forward references, and
* everything under `from __future__ import annotations`.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
let dir: string;
let cg: CodeGraph;
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1684-'));
fs.mkdirSync(path.join(dir, 'pkg'));
fs.writeFileSync(path.join(dir, 'pkg', '__init__.py'), '');
fs.writeFileSync(
path.join(dir, 'pkg', 'a.py'),
'def render(x):\n return x\n\nclass Alpha:\n def render(self):\n return "a"\n\nclass Beta:\n def render(self):\n return "b"\n'
);
fs.writeFileSync(
path.join(dir, 'pkg', 'b.py'),
'from __future__ import annotations\nfrom pkg.a import Alpha, Beta\n\n' +
'def quoted(o: "Alpha"):\n return o.render()\n\n' +
"def single_quoted(o: 'Beta'):\n return o.render()\n\n" +
'def unquoted(o: Alpha):\n return o.render()\n'
);
cg = CodeGraph.initSync(dir);
await cg.indexAll();
});
afterAll(() => {
cg.destroy();
fs.rmSync(dir, { recursive: true, force: true });
});
const calleeOf = (fn: string): string[] =>
cg
.getCallees(cg.getNodesByName(fn).find((n) => n.kind === 'function')!.id)
.map(({ node }) => node.qualifiedName)
.sort();
describe('quoted forward-reference annotations (#1684)', () => {
it('resolves the method on the quoted type, the same as the unquoted annotation', () => {
expect(calleeOf('unquoted')).toEqual(['Alpha::render']);
expect(calleeOf('quoted')).toEqual(['Alpha::render']);
expect(calleeOf('single_quoted')).toEqual(['Beta::render']);
});
});