Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- In Python, a call on a class or on a receiver named after it, like `Repo.save()` or `repo.save()` with `class Repo(Base[User])`, no longer links to a method of the base's type argument, like `User.save`. Before, every name in the base list counted as a supertype, so `User` was searched as if `Repo` inherited from it. The base itself, `Base`, is still searched. Re-index Python projects after upgrading.
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
Expand Down
110 changes: 110 additions & 0 deletions __tests__/python-supertype-type-arguments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* A Python class's supertypes, as its head declares them, are the classes its
* bases name — not their type arguments. `class Repo(Base[User])` extends
* `Base`; reading every word of the base list also counted `User`, so a call on
* the class or on a receiver named after it, `Repo.save()` / `repo.save()`,
* reached `User.save` when `Base` declares no `save`.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-py-supertype-type-args-'));
fs.mkdirSync(path.join(root, 'app'));
fs.writeFileSync(path.join(root, 'app/__init__.py'), '');
fs.writeFileSync(path.join(root, 'app/m.py'), `from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")


class Base(Generic[T]):
def load(self):
pass


class Pair(Generic[T, U]):
def load(self):
pass


class User:
def save(self):
pass


class Other:
def save(self):
pass


class Repo(Base[User]):
pass


class Store(Pair[str, dict[str, User]]):
pass


class Keyed(Base[User], metaclass=type):
pass


def save_by_class():
Repo.save()


def save_by_receiver(repo):
repo.save()


def save_nested():
Store.save()


def save_keyed():
Keyed.save()


def load_by_class():
Repo.load()


def load_nested():
Store.load()
`);
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** Qualified names of the methods `fn` calls. */
const calls = (fn: string): string[] => {
const node = cg.getNodesInFile('app/m.py').find((n) => n.name === fn && n.kind === 'function')!;
expect(node, fn).toBeDefined();
return cg.getOutgoingEdges(node.id).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!).filter((n) => n.kind === 'method').map((n) => n.qualifiedName);
};

describe('a Python type argument is no supertype', () => {
it('does not reach a method of the type argument', () => {
expect(calls('save_by_class')).not.toContain('User::save');
expect(calls('save_by_receiver')).not.toContain('User::save');
expect(calls('save_nested')).not.toContain('User::save');
expect(calls('save_keyed')).not.toContain('User::save');
});

it('still reaches the method the base declares', () => {
expect(calls('load_by_class')).toEqual(['Base::load']);
expect(calls('load_nested')).toEqual(['Pair::load']);
});
});
8 changes: 6 additions & 2 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19865,8 +19865,12 @@ function readDeclaredSupertypes(cls: Node, context: ResolutionContext): string[]
// A base built by a class's factory — Django's `class Manager(
// BaseManager.from_queryset(QuerySet))` — subclasses that class: the
// qualifier is the supertype, not the method called on it.
case 'python': return names(/\bclass\s+\w+\s*\(([^)]*)\)/.exec(head)?.[1]?.replace(/\w+\s*=\s*[\w.]+/g, '')
.replace(/([A-Za-z_][\w.]*)\.\w+\s*\(/g, '$1 ('));
// A parameterized base, `Base[User]`, is `Base`: its type arguments are no supertypes.
case 'python': {
let bases = /\bclass\s+\w+\s*\(([^)]*)\)/.exec(head)?.[1];
while (bases && /\[[^[\]]*\]/.test(bases)) bases = bases.replace(/\[[^[\]]*\]/g, '');
return names(bases?.replace(/\w+\s*=\s*[\w.]+/g, '').replace(/([A-Za-z_][\w.]*)\.\w+\s*\(/g, '$1 ('));
}
case 'ruby': return names(/\bclass\s+[\w:]+\s*<\s*([\w:]+)/.exec(head)?.[1]);
case 'php': return names(/\bextends\s+([\w.\\]+)/.exec(head)?.[1]);
case 'typescript': case 'tsx': case 'javascript': case 'jsx':
Expand Down