fix(python): a base's type argument is no supertype, so Repo.save() is not User.save

The Python branch of declaredSupertypes read every word of a class's base
list, so class Repo(Base[User]) declared both Base and User. A call on the
class or on a receiver named after it, Repo.save() or repo.save(), walks those
supertypes for the method (classStrategyMethod to inheritedClassMethod), and
when Base declares no save it reached User.save at 0.85 or 0.8. In
class Mixed(Base[Optional[User]], Mixin) the type argument also came ahead of
Mixin, so Mixed.save() reached User.save instead of Mixin.save.

Bracketed type arguments, nested ones and quoted forward references too, are
now dropped before the names are read. The base itself is still read and
searched. Other languages are untouched.

Validation:
- The new test file: red on main (the type-argument and later-base cases
  fail, the case that a base's own method is still reached passes), green here.
  We added the Mixed and quoted forward-reference cases to the contributor's test.
- A/B against main on 7 Python repos, 670,226 edges on main: requests, flask,
  litestar, mypy, polar's server, django unchanged, sqlalchemy 1 edge removed,
  0 added, 0 retargeted. That edge is a.references(b) in relationships.py. Its
  local a named a test's class a(TypedColumns), and TypedColumns' type argument
  "Column[Any]" reached Column.references: the right method by chance, by the
  path this fixes. Its twin b.references(a) two lines down has no edge in
  either arm.
- Controls express and gin: full dumps byte-identical.
- Kernel parity on flask and requests: 43/43 in both arms (the kernel is not
  touched). Index cost on sqlalchemy, median of 3 interleaved: 29.5 s on main,
  25.7 s here, so nothing measurable.

Original PR #3000 by @JosefAschauer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Josh66
2026-10-30 17:05:11 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 3f66df358e
commit cd04b21a31
3 changed files with 143 additions and 2 deletions
+6 -2
View File
@@ -21514,8 +21514,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':