From 96ac1533197eadcd743c232ddd5cd9b8aab022ca Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Fri, 4 Sep 2026 12:07:06 +0300 Subject: [PATCH] fix(resolution): read a quoted Python annotation as a receiver type (#1684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 at all. Admit the quoted form. --- CHANGELOG.md | 2 + __tests__/python-quoted-annotation.test.ts | 55 ++++++++++++++++++++++ src/resolution/name-matcher.ts | 6 +++ 3 files changed, 63 insertions(+) create mode 100644 __tests__/python-quoted-annotation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..17060be77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### Symbols, tests and the viewer +- Python parameters annotated with a quoted forward reference — `def f(o: "Alpha")`, or anything under `from __future__ import annotations` — now resolve the methods called on them, the same as the unquoted annotation. Re-index Python projects after upgrading. (#1684) + - **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges. - **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does. diff --git a/__tests__/python-quoted-annotation.test.ts b/__tests__/python-quoted-annotation.test.ts new file mode 100644 index 000000000..fb0a6db1c --- /dev/null +++ b/__tests__/python-quoted-annotation.test.ts @@ -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']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c74d8f272..d2c38b5c3 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1338,6 +1338,12 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[] case 'python': return [ new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // lg = Logger(...) + // A quoted forward reference (`lg: "Logger"`, `lg: 'pkg.Logger'`) is the + // same annotation — and what every file under `from __future__ import + // annotations` or with a not-yet-defined class writes. The unquoted + // pattern below stopped at the quote and read no type at all, so the + // call produced no edge (#1684). Tried first: it is the stricter shape. + new RegExp(`\\b${r}\\b\\s*:\\s*["']([A-Z][\\w.]*)["']`), // lg: "Logger" new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // lg: Logger (PEP 526) ]; case 'java':