[ty] Implement the legacy PEP-484 convention for indicating positional-only parameters (#20248)
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2025-09-05 17:56:06 +01:00 committed by GitHub
parent eb6154f792
commit 5d52902e18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 376 additions and 150 deletions

View file

@ -1,6 +1,6 @@
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, ParameterWithDefault};
use ruff_python_semantic::analyze::function_type; use ruff_python_semantic::analyze::function_type;
use crate::Violation; use crate::Violation;
@ -85,16 +85,9 @@ pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast
function_type::FunctionType::Method | function_type::FunctionType::ClassMethod function_type::FunctionType::Method | function_type::FunctionType::ClassMethod
)); ));
if let Some(arg) = function_def.parameters.args.get(skip) { if let Some(param) = function_def.parameters.args.get(skip) {
if is_old_style_positional_only(arg) { if param.uses_pep_484_positional_only_convention() {
checker.report_diagnostic(Pep484StylePositionalOnlyParameter, arg.identifier()); checker.report_diagnostic(Pep484StylePositionalOnlyParameter, param.identifier());
} }
} }
} }
/// Returns `true` if the [`ParameterWithDefault`] is an old-style positional-only parameter (i.e.,
/// its name starts with `__` and does not end with `__`).
fn is_old_style_positional_only(param: &ParameterWithDefault) -> bool {
let arg_name = param.name();
arg_name.starts_with("__") && !arg_name.ends_with("__")
}

View file

@ -3219,7 +3219,6 @@ impl<'a> IntoIterator for &'a Box<Parameters> {
/// Used by `Arguments` original type. /// Used by `Arguments` original type.
/// ///
/// NOTE: This type is different from original Python AST. /// NOTE: This type is different from original Python AST.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct ParameterWithDefault { pub struct ParameterWithDefault {
@ -3241,6 +3240,14 @@ impl ParameterWithDefault {
pub fn annotation(&self) -> Option<&Expr> { pub fn annotation(&self) -> Option<&Expr> {
self.parameter.annotation() self.parameter.annotation()
} }
/// Return `true` if the parameter name uses the pre-PEP-570 convention
/// (specified in PEP 484) to indicate to a type checker that it should be treated
/// as positional-only.
pub fn uses_pep_484_positional_only_convention(&self) -> bool {
let name = self.name();
name.starts_with("__") && !name.ends_with("__")
}
} }
/// An AST node used to represent the arguments passed to a function call or class definition. /// An AST node used to represent the arguments passed to a function call or class definition.

View file

@ -68,6 +68,78 @@ def _(flag: bool):
reveal_type(foo()) # revealed: int reveal_type(foo()) # revealed: int
``` ```
## PEP-484 convention for positional-only parameters
PEP 570, introduced in Python 3.8, added dedicated Python syntax for denoting positional-only
parameters (the `/` in a function signature). However, functions implemented in C were able to have
positional-only parameters prior to Python 3.8 (there was just no syntax for expressing this at the
Python level).
Stub files describing functions implemented in C nonetheless needed a way of expressing that certain
parameters were positional-only. In the absence of dedicated Python syntax, PEP 484 described a
convention that type checkers were expected to understand:
> Some functions are designed to take their arguments only positionally, and expect their callers
> never to use the arguments name to provide that argument by keyword. All arguments with names
> beginning with `__` are assumed to be positional-only, except if their names also end with `__`.
While this convention is now redundant (following the implementation of PEP 570), many projects
still continue to use the old convention, so it is supported by ty as well.
```py
def f(__x: int): ...
f(1)
# error: [missing-argument]
# error: [unknown-argument]
f(__x=1)
```
But not if they follow a non-positional-only parameter:
```py
def g(x: int, __y: str): ...
g(x=1, __y="foo")
```
And also not if they both start and end with `__`:
```py
def h(__x__: str): ...
h(__x__="foo")
```
And if *any* parameters use the new PEP-570 convention, the old convention does not apply:
```py
def i(x: str, /, __y: int): ...
i("foo", __y=42) # fine
```
And `self`/`cls` are implicitly positional-only:
```py
class C:
def method(self, __x: int): ...
@classmethod
def class_method(cls, __x: str): ...
# (the name of the first parameter is irrelevant;
# a staticmethod works the same as a free function in the global scope)
@staticmethod
def static_method(self, __x: int): ...
# error: [missing-argument]
# error: [unknown-argument]
C().method(__x=1)
# error: [missing-argument]
# error: [unknown-argument]
C.class_method(__x="1")
C.static_method("x", __x=42) # fine
```
## Splatted arguments ## Splatted arguments
### Unknown argument length ### Unknown argument length
@ -545,7 +617,7 @@ def _(args: str) -> None:
This is a regression that was highlighted by the ecosystem check, which shows that we might need to This is a regression that was highlighted by the ecosystem check, which shows that we might need to
rethink how we perform argument expansion during overload resolution. In particular, we might need rethink how we perform argument expansion during overload resolution. In particular, we might need
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry to retry both `match_parameters` *and* `check_types` for each expansion. Currently we only retry
`check_types`. `check_types`.
The issue is that argument expansion might produce a splatted value with a different arity than what The issue is that argument expansion might produce a splatted value with a different arity than what

View file

@ -413,13 +413,13 @@ To see the kinds and types of the protocol members, you can use the debugging ai
from ty_extensions import reveal_protocol_interface from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator
# revealed: {"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }} # revealed: {"method_member": MethodMember(`(self, /) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self, /) -> str` }, "z": PropertyMember { getter: `def z(self, /) -> int`, setter: `def z(self, /, z: int) -> None` }}
reveal_protocol_interface(Foo) reveal_protocol_interface(Foo)
# revealed: {"__index__": MethodMember(`(self) -> int`)} # revealed: {"__index__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(SupportsIndex) reveal_protocol_interface(SupportsIndex)
# revealed: {"__abs__": MethodMember(`(self) -> Unknown`)} # revealed: {"__abs__": MethodMember(`(self, /) -> Unknown`)}
reveal_protocol_interface(SupportsAbs) reveal_protocol_interface(SupportsAbs)
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)} # revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[Unknown]`), "__next__": MethodMember(`(self, /) -> Unknown`)}
reveal_protocol_interface(Iterator) reveal_protocol_interface(Iterator)
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`" # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
@ -439,9 +439,9 @@ do not implement any special handling for generic aliases passed to the function
reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str]
reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str]
# revealed: {"__abs__": MethodMember(`(self) -> int`)} # revealed: {"__abs__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(SupportsAbs[int]) reveal_protocol_interface(SupportsAbs[int])
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)} # revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[int]`), "__next__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(Iterator[int]) reveal_protocol_interface(Iterator[int])
class BaseProto(Protocol): class BaseProto(Protocol):
@ -450,10 +450,10 @@ class BaseProto(Protocol):
class SubProto(BaseProto, Protocol): class SubProto(BaseProto, Protocol):
def member(self) -> bool: ... def member(self) -> bool: ...
# revealed: {"member": MethodMember(`(self) -> int`)} # revealed: {"member": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(BaseProto) reveal_protocol_interface(BaseProto)
# revealed: {"member": MethodMember(`(self) -> bool`)} # revealed: {"member": MethodMember(`(self, /) -> bool`)}
reveal_protocol_interface(SubProto) reveal_protocol_interface(SubProto)
class ProtoWithClassVar(Protocol): class ProtoWithClassVar(Protocol):
@ -1767,7 +1767,7 @@ class Foo(Protocol):
def method(self) -> str: ... def method(self) -> str: ...
def f(x: Foo): def f(x: Foo):
reveal_type(type(x).method) # revealed: def method(self) -> str reveal_type(type(x).method) # revealed: def method(self, /) -> str
class Bar: class Bar:
def __init__(self): def __init__(self):
@ -1776,6 +1776,31 @@ class Bar:
f(Bar()) # error: [invalid-argument-type] f(Bar()) # error: [invalid-argument-type]
``` ```
Some protocols use the old convention (specified in PEP-484) for denoting positional-only
parameters. This is supported by ty:
```py
class HasPosOnlyDunders:
def __invert__(self, /) -> "HasPosOnlyDunders":
return self
def __lt__(self, other, /) -> bool:
return True
class SupportsLessThan(Protocol):
def __lt__(self, __other) -> bool: ...
class Invertable(Protocol):
# `self` and `cls` are always implicitly positional-only for methods defined in `Protocol`
# classes, even if no parameters in the method use the PEP-484 convention.
def __invert__(self) -> object: ...
static_assert(is_assignable_to(HasPosOnlyDunders, SupportsLessThan))
static_assert(is_assignable_to(HasPosOnlyDunders, Invertable))
static_assert(is_assignable_to(str, SupportsLessThan))
static_assert(is_assignable_to(int, Invertable))
```
## Equivalence of protocols with method or property members ## Equivalence of protocols with method or property members
Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the

View file

@ -49,6 +49,12 @@ pub struct AstNodeRef<T> {
_node: PhantomData<T>, _node: PhantomData<T>,
} }
impl<T> AstNodeRef<T> {
pub(crate) fn index(&self) -> NodeIndex {
self.index
}
}
impl<T> AstNodeRef<T> impl<T> AstNodeRef<T>
where where
T: HasNodeIndex + Ranged + PartialEq + Debug, T: HasNodeIndex + Ranged + PartialEq + Debug,

View file

@ -1,5 +1,7 @@
use ruff_python_ast::{HasNodeIndex, NodeIndex}; use ruff_python_ast::{HasNodeIndex, NodeIndex};
use crate::ast_node_ref::AstNodeRef;
/// Compact key for a node for use in a hash map. /// Compact key for a node for use in a hash map.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
pub(super) struct NodeKey(NodeIndex); pub(super) struct NodeKey(NodeIndex);
@ -11,4 +13,8 @@ impl NodeKey {
{ {
NodeKey(node.node_index().load()) NodeKey(node.node_index().load())
} }
pub(super) fn from_node_ref<T>(node_ref: &AstNodeRef<T>) -> Self {
NodeKey(node_ref.index())
}
} }

View file

@ -159,7 +159,6 @@ pub(crate) fn attribute_scopes<'db, 's>(
class_body_scope: ScopeId<'db>, class_body_scope: ScopeId<'db>,
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> { ) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
let file = class_body_scope.file(db); let file = class_body_scope.file(db);
let module = parsed_module(db, file).load(db);
let index = semantic_index(db, file); let index = semantic_index(db, file);
let class_scope_id = class_body_scope.file_scope_id(db); let class_scope_id = class_body_scope.file_scope_id(db);
@ -175,7 +174,7 @@ pub(crate) fn attribute_scopes<'db, 's>(
(child_scope_id, scope) (child_scope_id, scope)
}; };
function_scope.node().as_function(&module)?; function_scope.node().as_function()?;
Some(function_scope_id) Some(function_scope_id)
}) })
} }
@ -332,6 +331,39 @@ impl<'db> SemanticIndex<'db> {
Some(&self.scopes[self.parent_scope_id(scope_id)?]) Some(&self.scopes[self.parent_scope_id(scope_id)?])
} }
/// Return the [`Definition`] of the class enclosing this method, given the
/// method's body scope, or `None` if it is not a method.
pub(crate) fn class_definition_of_method(
&self,
function_body_scope: FileScopeId,
) -> Option<Definition<'db>> {
let current_scope = self.scope(function_body_scope);
if current_scope.kind() != ScopeKind::Function {
return None;
}
let parent_scope_id = current_scope.parent()?;
let parent_scope = self.scope(parent_scope_id);
let class_scope = match parent_scope.kind() {
ScopeKind::Class => parent_scope,
ScopeKind::TypeParams => {
let class_scope_id = parent_scope.parent()?;
let potentially_class_scope = self.scope(class_scope_id);
match potentially_class_scope.kind() {
ScopeKind::Class => potentially_class_scope,
_ => return None,
}
}
_ => return None,
};
class_scope
.node()
.as_class()
.map(|node_ref| self.expect_single_definition(node_ref))
}
fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool { fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool {
self.parent_scope_id(scope_id) self.parent_scope_id(scope_id)
.is_none_or(|parent_scope_id| { .is_none_or(|parent_scope_id| {

View file

@ -2644,7 +2644,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
match scope.kind() { match scope.kind() {
ScopeKind::Class | ScopeKind::Lambda => return false, ScopeKind::Class | ScopeKind::Lambda => return false,
ScopeKind::Function => { ScopeKind::Function => {
return scope.node().expect_function(self.module).is_async; return scope.node().expect_function().node(self.module).is_async;
} }
ScopeKind::Comprehension ScopeKind::Comprehension
| ScopeKind::Module | ScopeKind::Module

View file

@ -1227,3 +1227,12 @@ impl From<&ast::TypeParamTypeVarTuple> for DefinitionNodeKey {
Self(NodeKey::from_node(value)) Self(NodeKey::from_node(value))
} }
} }
impl<T> From<&AstNodeRef<T>> for DefinitionNodeKey
where
for<'a> &'a T: Into<DefinitionNodeKey>,
{
fn from(value: &AstNodeRef<T>) -> Self {
Self(NodeKey::from_node_ref(value))
}
}

View file

@ -397,52 +397,38 @@ impl NodeWithScopeKind {
} }
} }
pub(crate) fn expect_class<'ast>( pub(crate) fn as_class(&self) -> Option<&AstNodeRef<ast::StmtClassDef>> {
&self,
module: &'ast ParsedModuleRef,
) -> &'ast ast::StmtClassDef {
match self { match self {
Self::Class(class) => class.node(module), Self::Class(class) => Some(class),
_ => panic!("expected class"),
}
}
pub(crate) fn as_class<'ast>(
&self,
module: &'ast ParsedModuleRef,
) -> Option<&'ast ast::StmtClassDef> {
match self {
Self::Class(class) => Some(class.node(module)),
_ => None, _ => None,
} }
} }
pub(crate) fn expect_function<'ast>( pub(crate) fn expect_class(&self) -> &AstNodeRef<ast::StmtClassDef> {
&self, self.as_class().expect("expected class")
module: &'ast ParsedModuleRef,
) -> &'ast ast::StmtFunctionDef {
self.as_function(module).expect("expected function")
} }
pub(crate) fn expect_type_alias<'ast>( pub(crate) fn as_function(&self) -> Option<&AstNodeRef<ast::StmtFunctionDef>> {
&self,
module: &'ast ParsedModuleRef,
) -> &'ast ast::StmtTypeAlias {
match self { match self {
Self::TypeAlias(type_alias) => type_alias.node(module), Self::Function(function) => Some(function),
_ => panic!("expected type alias"),
}
}
pub(crate) fn as_function<'ast>(
&self,
module: &'ast ParsedModuleRef,
) -> Option<&'ast ast::StmtFunctionDef> {
match self {
Self::Function(function) => Some(function.node(module)),
_ => None, _ => None,
} }
} }
pub(crate) fn expect_function(&self) -> &AstNodeRef<ast::StmtFunctionDef> {
self.as_function().expect("expected function")
}
pub(crate) fn as_type_alias(&self) -> Option<&AstNodeRef<ast::StmtTypeAlias>> {
match self {
Self::TypeAlias(type_alias) => Some(type_alias),
_ => None,
}
}
pub(crate) fn expect_type_alias(&self) -> &AstNodeRef<ast::StmtTypeAlias> {
self.as_type_alias().expect("expected type alias")
}
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]

View file

@ -5649,7 +5649,7 @@ impl<'db> Type<'db> {
SpecialFormType::TypingSelf => { SpecialFormType::TypingSelf => {
let module = parsed_module(db, scope_id.file(db)).load(db); let module = parsed_module(db, scope_id.file(db)).load(db);
let index = semantic_index(db, scope_id.file(db)); let index = semantic_index(db, scope_id.file(db));
let Some(class) = nearest_enclosing_class(db, index, scope_id, &module) else { let Some(class) = nearest_enclosing_class(db, index, scope_id) else {
return Err(InvalidTypeExpressionError { return Err(InvalidTypeExpressionError {
fallback_type: Type::unknown(), fallback_type: Type::unknown(),
invalid_expressions: smallvec::smallvec_inline![ invalid_expressions: smallvec::smallvec_inline![
@ -9364,9 +9364,7 @@ fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
impl<'db> PEP695TypeAliasType<'db> { impl<'db> PEP695TypeAliasType<'db> {
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let scope = self.rhs_scope(db); let scope = self.rhs_scope(db);
let module = parsed_module(db, scope.file(db)).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias();
let type_alias_stmt_node = scope.node(db).expect_type_alias(&module);
semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node)
} }
@ -9374,9 +9372,9 @@ impl<'db> PEP695TypeAliasType<'db> {
pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> {
let scope = self.rhs_scope(db); let scope = self.rhs_scope(db);
let module = parsed_module(db, scope.file(db)).load(db); let module = parsed_module(db, scope.file(db)).load(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias(&module); let type_alias_stmt_node = scope.node(db).expect_type_alias();
let definition = self.definition(db); let definition = self.definition(db);
definition_expression_type(db, definition, &type_alias_stmt_node.value) definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value)
} }
fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self {

View file

@ -1403,7 +1403,7 @@ impl<'db> ClassLiteral<'db> {
let scope = self.body_scope(db); let scope = self.body_scope(db);
let file = scope.file(db); let file = scope.file(db);
let parsed = parsed_module(db, file).load(db); let parsed = parsed_module(db, file).load(db);
let class_def_node = scope.node(db).expect_class(&parsed); let class_def_node = scope.node(db).expect_class().node(&parsed);
class_def_node.type_params.as_ref().map(|type_params| { class_def_node.type_params.as_ref().map(|type_params| {
let index = semantic_index(db, scope.file(db)); let index = semantic_index(db, scope.file(db));
let definition = index.expect_single_definition(class_def_node); let definition = index.expect_single_definition(class_def_node);
@ -1445,14 +1445,13 @@ impl<'db> ClassLiteral<'db> {
/// query depends on the AST of another file (bad!). /// query depends on the AST of another file (bad!).
fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef { fn node<'ast>(self, db: &'db dyn Db, module: &'ast ParsedModuleRef) -> &'ast ast::StmtClassDef {
let scope = self.body_scope(db); let scope = self.body_scope(db);
scope.node(db).expect_class(module) scope.node(db).expect_class().node(module)
} }
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let body_scope = self.body_scope(db); let body_scope = self.body_scope(db);
let module = parsed_module(db, body_scope.file(db)).load(db);
let index = semantic_index(db, body_scope.file(db)); let index = semantic_index(db, body_scope.file(db));
index.expect_single_definition(body_scope.node(db).expect_class(&module)) index.expect_single_definition(body_scope.node(db).expect_class())
} }
pub(crate) fn apply_specialization( pub(crate) fn apply_specialization(
@ -2870,8 +2869,8 @@ impl<'db> ClassLiteral<'db> {
let class_table = place_table(db, class_body_scope); let class_table = place_table(db, class_body_scope);
let is_valid_scope = |method_scope: ScopeId<'db>| { let is_valid_scope = |method_scope: ScopeId<'db>| {
if let Some(method_def) = method_scope.node(db).as_function(&module) { if let Some(method_def) = method_scope.node(db).as_function() {
let method_name = method_def.name.as_str(); let method_name = method_def.node(&module).name.as_str();
if let Place::Type(Type::FunctionLiteral(method_type), _) = if let Place::Type(Type::FunctionLiteral(method_type), _) =
class_symbol(db, class_body_scope, method_name).place class_symbol(db, class_body_scope, method_name).place
{ {
@ -2946,20 +2945,22 @@ impl<'db> ClassLiteral<'db> {
} }
// The attribute assignment inherits the reachability of the method which contains it // The attribute assignment inherits the reachability of the method which contains it
let is_method_reachable = let is_method_reachable = if let Some(method_def) = method_scope.node(db).as_function()
if let Some(method_def) = method_scope.node(db).as_function(&module) { {
let method = index.expect_single_definition(method_def); let method = index.expect_single_definition(method_def);
let method_place = class_table.symbol_id(&method_def.name).unwrap(); let method_place = class_table
class_map .symbol_id(&method_def.node(&module).name)
.all_reachable_symbol_bindings(method_place) .unwrap();
.find_map(|bind| { class_map
(bind.binding.is_defined_and(|def| def == method)) .all_reachable_symbol_bindings(method_place)
.then(|| class_map.binding_reachability(db, &bind)) .find_map(|bind| {
}) (bind.binding.is_defined_and(|def| def == method))
.unwrap_or(Truthiness::AlwaysFalse) .then(|| class_map.binding_reachability(db, &bind))
} else { })
Truthiness::AlwaysFalse .unwrap_or(Truthiness::AlwaysFalse)
}; } else {
Truthiness::AlwaysFalse
};
if is_method_reachable.is_always_false() { if is_method_reachable.is_always_false() {
continue; continue;
} }
@ -3323,7 +3324,7 @@ impl<'db> ClassLiteral<'db> {
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
let class_scope = self.body_scope(db); let class_scope = self.body_scope(db);
let module = parsed_module(db, class_scope.file(db)).load(db); let module = parsed_module(db, class_scope.file(db)).load(db);
let class_node = class_scope.node(db).expect_class(&module); let class_node = class_scope.node(db).expect_class().node(&module);
let class_name = &class_node.name; let class_name = &class_node.name;
TextRange::new( TextRange::new(
class_name.start(), class_name.start(),
@ -4784,8 +4785,7 @@ impl KnownClass {
// 2. The first parameter of the current function (typically `self` or `cls`) // 2. The first parameter of the current function (typically `self` or `cls`)
match overload.parameter_types() { match overload.parameter_types() {
[] => { [] => {
let Some(enclosing_class) = let Some(enclosing_class) = nearest_enclosing_class(db, index, scope)
nearest_enclosing_class(db, index, scope, module)
else { else {
BoundSuperError::UnavailableImplicitArguments BoundSuperError::UnavailableImplicitArguments
.report_diagnostic(context, call_expression.into()); .report_diagnostic(context, call_expression.into());

View file

@ -172,7 +172,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
// Inspect all ancestor function scopes by walking bottom up and infer the function's type. // Inspect all ancestor function scopes by walking bottom up and infer the function's type.
let mut function_scope_tys = index let mut function_scope_tys = index
.ancestor_scopes(scope_id) .ancestor_scopes(scope_id)
.filter_map(|(_, scope)| scope.node().as_function(self.module())) .filter_map(|(_, scope)| scope.node().as_function())
.map(|node| binding_type(self.db, index.expect_single_definition(node))) .map(|node| binding_type(self.db, index.expect_single_definition(node)))
.filter_map(Type::into_function_literal); .filter_map(Type::into_function_literal);

View file

@ -200,15 +200,15 @@ impl ClassDisplay<'_> {
match ancestor_scope.kind() { match ancestor_scope.kind() {
ScopeKind::Class => { ScopeKind::Class => {
if let Some(class_def) = node.as_class(&module_ast) { if let Some(class_def) = node.as_class() {
name_parts.push(class_def.name.as_str().to_string()); name_parts.push(class_def.node(&module_ast).name.as_str().to_string());
} }
} }
ScopeKind::Function => { ScopeKind::Function => {
if let Some(function_def) = node.as_function(&module_ast) { if let Some(function_def) = node.as_function() {
name_parts.push(format!( name_parts.push(format!(
"<locals of function '{}'>", "<locals of function '{}'>",
function_def.name.as_str() function_def.node(&module_ast).name.as_str()
)); ));
} }
} }

View file

@ -55,7 +55,7 @@ use bitflags::bitflags;
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span};
use ruff_db::files::{File, FileRange}; use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast; use ruff_python_ast::{self as ast, ParameterWithDefault};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::module_resolver::{KnownModule, file_to_module}; use crate::module_resolver::{KnownModule, file_to_module};
@ -63,7 +63,7 @@ use crate::place::{Boundness, Place, place_from_bindings};
use crate::semantic_index::ast_ids::HasScopedUseId; use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition; use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::ScopeId; use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::semantic_index; use crate::semantic_index::{FileScopeId, SemanticIndex, semantic_index};
use crate::types::call::{Binding, CallArguments}; use crate::types::call::{Binding, CallArguments};
use crate::types::constraints::{ConstraintSet, Constraints}; use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::context::InferContext; use crate::types::context::InferContext;
@ -80,7 +80,7 @@ use crate::types::{
BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType,
DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsEquivalentVisitor, KnownClass, NormalizedVisitor, SpecialFormType, Truthiness, Type, IsEquivalentVisitor, KnownClass, NormalizedVisitor, SpecialFormType, Truthiness, Type,
TypeMapping, TypeRelation, UnionBuilder, all_members, walk_type_mapping, TypeMapping, TypeRelation, UnionBuilder, all_members, binding_type, walk_type_mapping,
}; };
use crate::{Db, FxOrderSet, ModuleName, resolve_module}; use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@ -236,6 +236,22 @@ impl<'db> OverloadLiteral<'db> {
self.has_known_decorator(db, FunctionDecorators::OVERLOAD) self.has_known_decorator(db, FunctionDecorators::OVERLOAD)
} }
/// Returns true if this overload is decorated with `@staticmethod`, or if it is implicitly a
/// staticmethod.
pub(crate) fn is_staticmethod(self, db: &dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__"
}
/// Returns true if this overload is decorated with `@classmethod`, or if it is implicitly a
/// classmethod.
pub(crate) fn is_classmethod(self, db: &dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
|| matches!(
self.name(db).as_str(),
"__init_subclass__" | "__class_getitem__"
)
}
fn node<'ast>( fn node<'ast>(
self, self,
db: &dyn Db, db: &dyn Db,
@ -249,7 +265,7 @@ impl<'db> OverloadLiteral<'db> {
the function is defined." the function is defined."
); );
self.body_scope(db).node(db).expect_function(module) self.body_scope(db).node(db).expect_function().node(module)
} }
/// Returns the [`FileRange`] of the function's name. /// Returns the [`FileRange`] of the function's name.
@ -258,7 +274,8 @@ impl<'db> OverloadLiteral<'db> {
self.file(db), self.file(db),
self.body_scope(db) self.body_scope(db)
.node(db) .node(db)
.expect_function(module) .expect_function()
.node(module)
.name .name
.range, .range,
) )
@ -274,9 +291,8 @@ impl<'db> OverloadLiteral<'db> {
/// over-invalidation. /// over-invalidation.
fn definition(self, db: &'db dyn Db) -> Definition<'db> { fn definition(self, db: &'db dyn Db) -> Definition<'db> {
let body_scope = self.body_scope(db); let body_scope = self.body_scope(db);
let module = parsed_module(db, self.file(db)).load(db);
let index = semantic_index(db, body_scope.file(db)); let index = semantic_index(db, body_scope.file(db));
index.expect_single_definition(body_scope.node(db).expect_function(&module)) index.expect_single_definition(body_scope.node(db).expect_function())
} }
/// Returns the overload immediately before this one in the AST. Returns `None` if there is no /// Returns the overload immediately before this one in the AST. Returns `None` if there is no
@ -290,7 +306,8 @@ impl<'db> OverloadLiteral<'db> {
let use_id = self let use_id = self
.body_scope(db) .body_scope(db)
.node(db) .node(db)
.expect_function(&module) .expect_function()
.node(&module)
.name .name
.scoped_use_id(db, scope); .scoped_use_id(db, scope);
@ -325,17 +342,79 @@ impl<'db> OverloadLiteral<'db> {
db: &'db dyn Db, db: &'db dyn Db,
inherited_generic_context: Option<GenericContext<'db>>, inherited_generic_context: Option<GenericContext<'db>>,
) -> Signature<'db> { ) -> Signature<'db> {
/// `self` or `cls` can be implicitly positional-only if:
/// - It is a method AND
/// - No parameters in the method use PEP-570 syntax AND
/// - It is not a `@staticmethod` AND
/// - `self`/`cls` is not explicitly positional-only using the PEP-484 convention AND
/// - Either the next parameter after `self`/`cls` uses the PEP-484 convention,
/// or the enclosing class is a `Protocol` class
fn has_implicitly_positional_only_first_param<'db>(
db: &'db dyn Db,
literal: OverloadLiteral<'db>,
node: &ast::StmtFunctionDef,
scope: FileScopeId,
index: &SemanticIndex,
) -> bool {
let parameters = &node.parameters;
if !parameters.posonlyargs.is_empty() {
return false;
}
let Some(first_param) = parameters.args.first() else {
return false;
};
if first_param.uses_pep_484_positional_only_convention() {
return false;
}
if literal.is_staticmethod(db) {
return false;
}
let Some(class_definition) = index.class_definition_of_method(scope) else {
return false;
};
// `self` and `cls` are always positional-only if the next parameter uses the
// PEP-484 convention.
if parameters
.args
.get(1)
.is_some_and(ParameterWithDefault::uses_pep_484_positional_only_convention)
{
return true;
}
// If there isn't any parameter other than `self`/`cls`,
// or there is but it isn't using the PEP-484 convention,
// then `self`/`cls` are only implicitly positional-only if
// it is a protocol class.
let class_type = binding_type(db, class_definition);
class_type
.to_class_type(db)
.is_some_and(|class| class.is_protocol(db))
}
let scope = self.body_scope(db); let scope = self.body_scope(db);
let module = parsed_module(db, self.file(db)).load(db); let module = parsed_module(db, self.file(db)).load(db);
let function_stmt_node = scope.node(db).expect_function(&module); let function_stmt_node = scope.node(db).expect_function().node(&module);
let definition = self.definition(db); let definition = self.definition(db);
let index = semantic_index(db, scope.file(db));
let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| { let generic_context = function_stmt_node.type_params.as_ref().map(|type_params| {
let index = semantic_index(db, scope.file(db));
GenericContext::from_type_params(db, index, definition, type_params) GenericContext::from_type_params(db, index, definition, type_params)
}); });
let file_scope_id = scope.file_scope_id(db);
let index = semantic_index(db, scope.file(db)); let is_generator = file_scope_id.is_generator_function(index);
let is_generator = scope.file_scope_id(db).is_generator_function(index); let has_implicitly_positional_first_parameter = has_implicitly_positional_only_first_param(
db,
self,
function_stmt_node,
file_scope_id,
index,
);
Signature::from_function( Signature::from_function(
db, db,
@ -344,6 +423,7 @@ impl<'db> OverloadLiteral<'db> {
definition, definition,
function_stmt_node, function_stmt_node,
is_generator, is_generator,
has_implicitly_positional_first_parameter,
) )
} }
@ -356,7 +436,7 @@ impl<'db> OverloadLiteral<'db> {
let span = Span::from(function_scope.file(db)); let span = Span::from(function_scope.file(db));
let node = function_scope.node(db); let node = function_scope.node(db);
let module = parsed_module(db, self.file(db)).load(db); let module = parsed_module(db, self.file(db)).load(db);
let func_def = node.as_function(&module)?; let func_def = node.as_function()?.node(&module);
let range = parameter_index let range = parameter_index
.and_then(|parameter_index| { .and_then(|parameter_index| {
func_def func_def
@ -376,7 +456,7 @@ impl<'db> OverloadLiteral<'db> {
let span = Span::from(function_scope.file(db)); let span = Span::from(function_scope.file(db));
let node = function_scope.node(db); let node = function_scope.node(db);
let module = parsed_module(db, self.file(db)).load(db); let module = parsed_module(db, self.file(db)).load(db);
let func_def = node.as_function(&module)?; let func_def = node.as_function()?.node(&module);
let return_type_range = func_def.returns.as_ref().map(|returns| returns.range()); let return_type_range = func_def.returns.as_ref().map(|returns| returns.range());
let mut signature = func_def.name.range.cover(func_def.parameters.range); let mut signature = func_def.name.range.cover(func_def.parameters.range);
if let Some(return_type_range) = return_type_range { if let Some(return_type_range) = return_type_range {
@ -713,17 +793,15 @@ impl<'db> FunctionType<'db> {
/// Returns true if this method is decorated with `@classmethod`, or if it is implicitly a /// Returns true if this method is decorated with `@classmethod`, or if it is implicitly a
/// classmethod. /// classmethod.
pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool { pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) self.iter_overloads_and_implementation(db)
|| matches!( .any(|overload| overload.is_classmethod(db))
self.name(db).as_str(),
"__init_subclass__" | "__class_getitem__"
)
} }
/// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a /// Returns true if this method is decorated with `@staticmethod`, or if it is implicitly a
/// static method. /// static method.
pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool { pub(crate) fn is_staticmethod(self, db: &'db dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || self.name(db) == "__new__" self.iter_overloads_and_implementation(db)
.any(|overload| overload.is_staticmethod(db))
} }
/// If the implementation of this function is deprecated, returns the `@warnings.deprecated`. /// If the implementation of this function is deprecated, returns the `@warnings.deprecated`.

View file

@ -422,12 +422,11 @@ pub(crate) fn nearest_enclosing_class<'db>(
db: &'db dyn Db, db: &'db dyn Db,
semantic: &SemanticIndex<'db>, semantic: &SemanticIndex<'db>,
scope: ScopeId, scope: ScopeId,
parsed: &ParsedModuleRef,
) -> Option<ClassLiteral<'db>> { ) -> Option<ClassLiteral<'db>> {
semantic semantic
.ancestor_scopes(scope.file_scope_id(db)) .ancestor_scopes(scope.file_scope_id(db))
.find_map(|(_, ancestor_scope)| { .find_map(|(_, ancestor_scope)| {
let class = ancestor_scope.node().as_class(parsed)?; let class = ancestor_scope.node().as_class()?;
let definition = semantic.expect_single_definition(class); let definition = semantic.expect_single_definition(class);
infer_definition_types(db, definition) infer_definition_types(db, definition)
.declaration_type(definition) .declaration_type(definition)
@ -2418,29 +2417,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
/// behaviour to the [`nearest_enclosing_class`] function. /// behaviour to the [`nearest_enclosing_class`] function.
fn class_context_of_current_method(&self) -> Option<ClassType<'db>> { fn class_context_of_current_method(&self) -> Option<ClassType<'db>> {
let current_scope_id = self.scope().file_scope_id(self.db()); let current_scope_id = self.scope().file_scope_id(self.db());
let current_scope = self.index.scope(current_scope_id); let class_definition = self.index.class_definition_of_method(current_scope_id)?;
if current_scope.kind() != ScopeKind::Function {
return None;
}
let parent_scope_id = current_scope.parent()?;
let parent_scope = self.index.scope(parent_scope_id);
let class_scope = match parent_scope.kind() {
ScopeKind::Class => parent_scope,
ScopeKind::TypeParams => {
let class_scope_id = parent_scope.parent()?;
let potentially_class_scope = self.index.scope(class_scope_id);
match potentially_class_scope.kind() {
ScopeKind::Class => potentially_class_scope,
_ => return None,
}
}
_ => return None,
};
let class_stmt = class_scope.node().as_class(self.module())?;
let class_definition = self.index.expect_single_definition(class_stmt);
binding_type(self.db(), class_definition).to_class_type(self.db()) binding_type(self.db(), class_definition).to_class_type(self.db())
} }
@ -2453,7 +2430,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if !current_scope.kind().is_non_lambda_function() { if !current_scope.kind().is_non_lambda_function() {
return None; return None;
} }
current_scope.node().as_function(self.module()) current_scope
.node()
.as_function()
.map(|node_ref| node_ref.node(self.module()))
} }
fn function_decorator_types<'a>( fn function_decorator_types<'a>(

View file

@ -12,7 +12,7 @@
use std::{collections::HashMap, slice::Iter}; use std::{collections::HashMap, slice::Iter};
use itertools::EitherOrBoth; use itertools::{EitherOrBoth, Itertools};
use smallvec::{SmallVec, smallvec_inline}; use smallvec::{SmallVec, smallvec_inline};
use super::{DynamicType, Type, TypeVarVariance, definition_expression_type}; use super::{DynamicType, Type, TypeVarVariance, definition_expression_type};
@ -352,9 +352,14 @@ impl<'db> Signature<'db> {
definition: Definition<'db>, definition: Definition<'db>,
function_node: &ast::StmtFunctionDef, function_node: &ast::StmtFunctionDef,
is_generator: bool, is_generator: bool,
has_implicitly_positional_first_parameter: bool,
) -> Self { ) -> Self {
let parameters = let parameters = Parameters::from_parameters(
Parameters::from_parameters(db, definition, function_node.parameters.as_ref()); db,
definition,
function_node.parameters.as_ref(),
has_implicitly_positional_first_parameter,
);
let return_ty = function_node.returns.as_ref().map(|returns| { let return_ty = function_node.returns.as_ref().map(|returns| {
let plain_return_ty = definition_expression_type(db, definition, returns.as_ref()) let plain_return_ty = definition_expression_type(db, definition, returns.as_ref())
.apply_type_mapping( .apply_type_mapping(
@ -1139,6 +1144,7 @@ impl<'db> Parameters<'db> {
db: &'db dyn Db, db: &'db dyn Db,
definition: Definition<'db>, definition: Definition<'db>,
parameters: &ast::Parameters, parameters: &ast::Parameters,
has_implicitly_positional_first_parameter: bool,
) -> Self { ) -> Self {
let ast::Parameters { let ast::Parameters {
posonlyargs, posonlyargs,
@ -1149,23 +1155,46 @@ impl<'db> Parameters<'db> {
range: _, range: _,
node_index: _, node_index: _,
} = parameters; } = parameters;
let default_type = |param: &ast::ParameterWithDefault| { let default_type = |param: &ast::ParameterWithDefault| {
param param
.default() .default()
.map(|default| definition_expression_type(db, definition, default)) .map(|default| definition_expression_type(db, definition, default))
}; };
let positional_only = posonlyargs.iter().map(|arg| {
let pos_only_param = |param: &ast::ParameterWithDefault| {
Parameter::from_node_and_kind( Parameter::from_node_and_kind(
db, db,
definition, definition,
&arg.parameter, &param.parameter,
ParameterKind::PositionalOnly { ParameterKind::PositionalOnly {
name: Some(arg.parameter.name.id.clone()), name: Some(param.parameter.name.id.clone()),
default_type: default_type(arg), default_type: default_type(param),
}, },
) )
}); };
let positional_or_keyword = args.iter().map(|arg| {
let mut positional_only: Vec<Parameter> = posonlyargs.iter().map(pos_only_param).collect();
let mut pos_or_keyword_iter = args.iter();
// If there are no PEP-570 positional-only parameters, check for the legacy PEP-484 convention
// for denoting positional-only parameters (parameters that start with `__` and do not end with `__`)
if positional_only.is_empty() {
let pos_or_keyword_iter = pos_or_keyword_iter.by_ref();
if has_implicitly_positional_first_parameter {
positional_only.extend(pos_or_keyword_iter.next().map(pos_only_param));
}
positional_only.extend(
pos_or_keyword_iter
.peeking_take_while(|param| param.uses_pep_484_positional_only_convention())
.map(pos_only_param),
);
}
let positional_or_keyword = pos_or_keyword_iter.map(|arg| {
Parameter::from_node_and_kind( Parameter::from_node_and_kind(
db, db,
definition, definition,
@ -1176,6 +1205,7 @@ impl<'db> Parameters<'db> {
}, },
) )
}); });
let variadic = vararg.as_ref().map(|arg| { let variadic = vararg.as_ref().map(|arg| {
Parameter::from_node_and_kind( Parameter::from_node_and_kind(
db, db,
@ -1186,6 +1216,7 @@ impl<'db> Parameters<'db> {
}, },
) )
}); });
let keyword_only = kwonlyargs.iter().map(|arg| { let keyword_only = kwonlyargs.iter().map(|arg| {
Parameter::from_node_and_kind( Parameter::from_node_and_kind(
db, db,
@ -1197,6 +1228,7 @@ impl<'db> Parameters<'db> {
}, },
) )
}); });
let keywords = kwarg.as_ref().map(|arg| { let keywords = kwarg.as_ref().map(|arg| {
Parameter::from_node_and_kind( Parameter::from_node_and_kind(
db, db,
@ -1207,8 +1239,10 @@ impl<'db> Parameters<'db> {
}, },
) )
}); });
Self::new( Self::new(
positional_only positional_only
.into_iter()
.chain(positional_or_keyword) .chain(positional_or_keyword)
.chain(variadic) .chain(variadic)
.chain(keyword_only) .chain(keyword_only)