Merge branch 'dcreager/source-order-constraints' into dcreager/callable-return
* dcreager/source-order-constraints: (30 commits) clippy fix test expectations (again) include source_order in display_graph output place bounds/constraints first don't always bump only fold once document display source_order more comments remove now-unused items fix test expectation use source order in specialize_constrained too document overall approach more comment reuse self source_order sort specialize_constrained by source_order lots of renaming remove source_order_for simpler source_order_for doc restore TODOs ...
This commit is contained in:
@@ -201,7 +201,7 @@ python-version = "3.12"
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any, /) -> _SpecialForm
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method TypeAliasType.__or__(right: Any, /) -> _SpecialForm
|
||||
```
|
||||
|
||||
## Method calls on types not disjoint from `None`
|
||||
|
||||
@@ -567,7 +567,7 @@ def f(x: int):
|
||||
super(x, x)
|
||||
|
||||
type IntAlias = int
|
||||
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||
# error: [invalid-super-argument] "`TypeAliasType` is not a valid class"
|
||||
super(IntAlias, 0)
|
||||
|
||||
# error: [invalid-super-argument] "`str` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, str)` call"
|
||||
|
||||
@@ -521,6 +521,73 @@ frozen = MyFrozenChildClass()
|
||||
del frozen.x # TODO this should emit an [invalid-assignment]
|
||||
```
|
||||
|
||||
### frozen/non-frozen inheritance
|
||||
|
||||
If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We
|
||||
catch this error:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrozenBase:
|
||||
x: int
|
||||
|
||||
@dataclass
|
||||
# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
|
||||
class Child(FrozenBase):
|
||||
y: int
|
||||
```
|
||||
|
||||
Frozen dataclasses inheriting from non-frozen dataclasses are also illegal:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
|
||||
@dataclass(frozen=True)
|
||||
# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
|
||||
class FrozenChild(Base):
|
||||
y: int
|
||||
```
|
||||
|
||||
Example of diagnostics when there are multiple files involved:
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
import dataclasses
|
||||
|
||||
@dataclasses.dataclass(frozen=False)
|
||||
class NotFrozenBase:
|
||||
x: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
from typing import final
|
||||
from dataclasses import dataclass
|
||||
|
||||
from module import NotFrozenBase
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True)
|
||||
@total_ordering
|
||||
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
|
||||
y: str
|
||||
```
|
||||
|
||||
### `match_args`
|
||||
|
||||
If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
reveal_type(type(P)) # revealed: <class 'ParamSpec'>
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(P) # revealed: ParamSpec
|
||||
reveal_type(P.__name__) # revealed: Literal["P"]
|
||||
```
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
@@ -146,7 +146,7 @@ from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", default=int)
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__default__) # revealed: int
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
@@ -187,7 +187,7 @@ from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__bound__) # revealed: int
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
@@ -211,7 +211,7 @@ from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", int, str)
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__constraints__) # revealed: tuple[int, str]
|
||||
|
||||
S = TypeVar("S")
|
||||
|
||||
@@ -12,7 +12,7 @@ python-version = "3.13"
|
||||
|
||||
```py
|
||||
def foo1[**P]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(P) # revealed: ParamSpec
|
||||
```
|
||||
|
||||
## Bounds and constraints
|
||||
@@ -45,14 +45,14 @@ The default value for a `ParamSpec` can be either a list of types, `...`, or ano
|
||||
|
||||
```py
|
||||
def foo2[**P = ...]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(P) # revealed: ParamSpec
|
||||
|
||||
def foo3[**P = [int, str]]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(P) # revealed: ParamSpec
|
||||
|
||||
def foo4[**P, **Q = P]():
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(Q) # revealed: typing.ParamSpec
|
||||
reveal_type(P) # revealed: ParamSpec
|
||||
reveal_type(Q) # revealed: ParamSpec
|
||||
```
|
||||
|
||||
Other values are invalid.
|
||||
|
||||
@@ -17,7 +17,7 @@ instances of `typing.TypeVar`, just like legacy type variables.
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ python-version = "3.13"
|
||||
```py
|
||||
def f[T = int]():
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__default__) # revealed: int
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
@@ -66,7 +66,7 @@ class Invalid[S = T]: ...
|
||||
```py
|
||||
def f[T: int]():
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__bound__) # revealed: int
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
@@ -79,7 +79,7 @@ def g[S]():
|
||||
```py
|
||||
def f[T: (int, str)]():
|
||||
reveal_type(type(T)) # revealed: <class 'TypeVar'>
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T) # revealed: TypeVar
|
||||
reveal_type(T.__constraints__) # revealed: tuple[int, str]
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ reveal_type(ListOrTuple) # revealed: <types.UnionType special form 'list[T@List
|
||||
reveal_type(ListOrTupleLegacy)
|
||||
reveal_type(MyCallable) # revealed: <typing.Callable special form '(**P@MyCallable) -> T@MyCallable'>
|
||||
reveal_type(AnnotatedType) # revealed: <special form 'typing.Annotated[T@AnnotatedType, <metadata>]'>
|
||||
reveal_type(TransparentAlias) # revealed: typing.TypeVar
|
||||
reveal_type(TransparentAlias) # revealed: TypeVar
|
||||
reveal_type(MyOptional) # revealed: <types.UnionType special form 'T@MyOptional | None'>
|
||||
|
||||
def _(
|
||||
|
||||
@@ -12,7 +12,7 @@ python-version = "3.12"
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
|
||||
reveal_type(IntOrStr) # revealed: TypeAliasType
|
||||
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
|
||||
|
||||
x: IntOrStr = 1
|
||||
@@ -205,7 +205,7 @@ from typing_extensions import TypeAliasType, Union
|
||||
|
||||
IntOrStr = TypeAliasType("IntOrStr", Union[int, str])
|
||||
|
||||
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
|
||||
reveal_type(IntOrStr) # revealed: TypeAliasType
|
||||
|
||||
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ diagnostic message for `invalid-exception-caught` expects to construct `typing.P
|
||||
def foo[**P]() -> None:
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `typing.ParamSpec`"
|
||||
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `ParamSpec`"
|
||||
except P:
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Implicit class body attributes
|
||||
|
||||
## Class body implicit attributes
|
||||
|
||||
Python makes certain names available implicitly inside class body scopes. These are `__qualname__`,
|
||||
`__module__`, and `__doc__`, as documented at
|
||||
<https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
reveal_type(__qualname__) # revealed: str
|
||||
reveal_type(__module__) # revealed: str
|
||||
reveal_type(__doc__) # revealed: str | None
|
||||
```
|
||||
|
||||
## `__firstlineno__` (Python 3.13+)
|
||||
|
||||
Python 3.13 added `__firstlineno__` to the class body namespace.
|
||||
|
||||
### Available in Python 3.13+
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
reveal_type(__firstlineno__) # revealed: int
|
||||
```
|
||||
|
||||
### Not available in Python 3.12 and earlier
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
__firstlineno__
|
||||
```
|
||||
|
||||
## Nested classes
|
||||
|
||||
These implicit attributes are also available in nested classes, and refer to the nested class:
|
||||
|
||||
```py
|
||||
class Outer:
|
||||
class Inner:
|
||||
reveal_type(__qualname__) # revealed: str
|
||||
reveal_type(__module__) # revealed: str
|
||||
```
|
||||
|
||||
## Class body implicit attributes have priority over globals
|
||||
|
||||
If a global variable with the same name exists, the class body implicit attribute takes priority
|
||||
within the class body:
|
||||
|
||||
```py
|
||||
__qualname__ = 42
|
||||
__module__ = 42
|
||||
|
||||
class Foo:
|
||||
# Inside the class body, these are the implicit class attributes
|
||||
reveal_type(__qualname__) # revealed: str
|
||||
reveal_type(__module__) # revealed: str
|
||||
|
||||
# Outside the class, the globals are visible
|
||||
reveal_type(__qualname__) # revealed: Literal[42]
|
||||
reveal_type(__module__) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
## `__firstlineno__` has priority over globals (Python 3.13+)
|
||||
|
||||
The same applies to `__firstlineno__` on Python 3.13+:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
__firstlineno__ = "not an int"
|
||||
|
||||
class Foo:
|
||||
reveal_type(__firstlineno__) # revealed: int
|
||||
|
||||
reveal_type(__firstlineno__) # revealed: Literal["not an int"]
|
||||
```
|
||||
|
||||
## Class body implicit attributes are not visible in methods
|
||||
|
||||
The implicit class body attributes are only available directly in the class body, not in nested
|
||||
function scopes (methods):
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# Available directly in the class body
|
||||
x = __qualname__
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
def method(self):
|
||||
# Not available in methods - falls back to builtins/globals
|
||||
# error: [unresolved-reference]
|
||||
__qualname__
|
||||
```
|
||||
|
||||
## Real-world use case: logging
|
||||
|
||||
A common use case is defining a logger with the class name:
|
||||
|
||||
```py
|
||||
import logging
|
||||
|
||||
class MyClass:
|
||||
logger = logging.getLogger(__qualname__)
|
||||
reveal_type(logger) # revealed: Logger
|
||||
```
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: dataclasses.md - Dataclasses - Other dataclass parameters - frozen/non-frozen inheritance
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## a.py
|
||||
|
||||
```
|
||||
1 | from dataclasses import dataclass
|
||||
2 |
|
||||
3 | @dataclass(frozen=True)
|
||||
4 | class FrozenBase:
|
||||
5 | x: int
|
||||
6 |
|
||||
7 | @dataclass
|
||||
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
|
||||
9 | class Child(FrozenBase):
|
||||
10 | y: int
|
||||
```
|
||||
|
||||
## b.py
|
||||
|
||||
```
|
||||
1 | from dataclasses import dataclass
|
||||
2 |
|
||||
3 | @dataclass
|
||||
4 | class Base:
|
||||
5 | x: int
|
||||
6 |
|
||||
7 | @dataclass(frozen=True)
|
||||
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
|
||||
9 | class FrozenChild(Base):
|
||||
10 | y: int
|
||||
```
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | import dataclasses
|
||||
2 |
|
||||
3 | @dataclasses.dataclass(frozen=False)
|
||||
4 | class NotFrozenBase:
|
||||
5 | x: int
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from functools import total_ordering
|
||||
2 | from typing import final
|
||||
3 | from dataclasses import dataclass
|
||||
4 |
|
||||
5 | from module import NotFrozenBase
|
||||
6 |
|
||||
7 | @final
|
||||
8 | @dataclass(frozen=True)
|
||||
9 | @total_ordering
|
||||
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
|
||||
11 | y: str
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass
|
||||
--> src/a.py:7:1
|
||||
|
|
||||
5 | x: int
|
||||
6 |
|
||||
7 | @dataclass
|
||||
| ---------- `Child` dataclass parameters
|
||||
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
|
||||
9 | class Child(FrozenBase):
|
||||
| ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is
|
||||
10 | y: int
|
||||
|
|
||||
info: This causes the class creation to fail
|
||||
info: Base class definition
|
||||
--> src/a.py:3:1
|
||||
|
|
||||
1 | from dataclasses import dataclass
|
||||
2 |
|
||||
3 | @dataclass(frozen=True)
|
||||
| ----------------------- `FrozenBase` dataclass parameters
|
||||
4 | class FrozenBase:
|
||||
| ^^^^^^^^^^ `FrozenBase` definition
|
||||
5 | x: int
|
||||
|
|
||||
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
|
||||
--> src/b.py:7:1
|
||||
|
|
||||
5 | x: int
|
||||
6 |
|
||||
7 | @dataclass(frozen=True)
|
||||
| ----------------------- `FrozenChild` dataclass parameters
|
||||
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
|
||||
9 | class FrozenChild(Base):
|
||||
| ^^^^^^^^^^^^----^ Subclass `FrozenChild` is frozen but base class `Base` is not
|
||||
10 | y: int
|
||||
|
|
||||
info: This causes the class creation to fail
|
||||
info: Base class definition
|
||||
--> src/b.py:3:1
|
||||
|
|
||||
1 | from dataclasses import dataclass
|
||||
2 |
|
||||
3 | @dataclass
|
||||
| ---------- `Base` dataclass parameters
|
||||
4 | class Base:
|
||||
| ^^^^ `Base` definition
|
||||
5 | x: int
|
||||
|
|
||||
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
|
||||
--> src/main.py:8:1
|
||||
|
|
||||
7 | @final
|
||||
8 | @dataclass(frozen=True)
|
||||
| ----------------------- `FrozenChild` dataclass parameters
|
||||
9 | @total_ordering
|
||||
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
|
||||
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
|
||||
11 | y: str
|
||||
|
|
||||
info: This causes the class creation to fail
|
||||
info: Base class definition
|
||||
--> src/module.py:3:1
|
||||
|
|
||||
1 | import dataclasses
|
||||
2 |
|
||||
3 | @dataclasses.dataclass(frozen=False)
|
||||
| ------------------------------------ `NotFrozenBase` dataclass parameters
|
||||
4 | class NotFrozenBase:
|
||||
| ^^^^^^^^^^^^^ `NotFrozenBase` definition
|
||||
5 | x: int
|
||||
|
|
||||
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
|
||||
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
||||
@@ -1633,6 +1634,35 @@ mod implicit_globals {
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up the type of an "implicit class body symbol". Returns [`Place::Undefined`] if
|
||||
/// `name` is not present as an implicit symbol in class bodies.
|
||||
///
|
||||
/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, `__doc__`,
|
||||
/// and `__firstlineno__` that Python implicitly makes available inside a class body during
|
||||
/// class creation.
|
||||
///
|
||||
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>
|
||||
pub(crate) fn class_body_implicit_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
match name {
|
||||
"__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||
"__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||
// __doc__ is `str` if there's a docstring, `None` if there isn't
|
||||
"__doc__" => Place::bound(UnionType::from_elements(
|
||||
db,
|
||||
[KnownClass::Str.to_instance(db), Type::none(db)],
|
||||
))
|
||||
.into(),
|
||||
// __firstlineno__ was added in Python 3.13
|
||||
"__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => {
|
||||
Place::bound(KnownClass::Int.to_instance(db)).into()
|
||||
}
|
||||
_ => Place::Undefined.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
|
||||
@@ -690,6 +690,12 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
impl DataclassFlags {
|
||||
pub(crate) const fn is_frozen(self) -> bool {
|
||||
self.contains(Self::FROZEN)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DATACLASS_FLAGS: &[(&str, DataclassFlags)] = &[
|
||||
("init", DataclassFlags::INIT),
|
||||
("repr", DataclassFlags::REPR),
|
||||
|
||||
@@ -689,20 +689,10 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn order_elements(mut self, val: bool) -> Self {
|
||||
self.order_elements = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn add_positive(self, ty: Type<'db>) -> Self {
|
||||
self.add_positive_impl(ty, &mut vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn add_positive_in_place(&mut self, ty: Type<'db>) {
|
||||
let updated = std::mem::replace(self, Self::empty(self.db)).add_positive(ty);
|
||||
*self = updated;
|
||||
}
|
||||
|
||||
pub(crate) fn add_positive_impl(
|
||||
mut self,
|
||||
ty: Type<'db>,
|
||||
|
||||
@@ -4659,15 +4659,6 @@ fn asynccontextmanager_return_type<'db>(db: &'db dyn Db, func_ty: Type<'db>) ->
|
||||
.ok()?
|
||||
.homogeneous_element_type(db);
|
||||
|
||||
if yield_ty.is_divergent()
|
||||
|| signature
|
||||
.parameters()
|
||||
.iter()
|
||||
.any(|param| param.annotated_type().is_some_and(|ty| ty.is_divergent()))
|
||||
{
|
||||
return Some(yield_ty);
|
||||
}
|
||||
|
||||
let context_manager =
|
||||
known_module_symbol(db, KnownModule::Contextlib, "_AsyncGeneratorContextManager")
|
||||
.place
|
||||
|
||||
@@ -1871,6 +1871,28 @@ impl<'db> ClassLiteral<'db> {
|
||||
.filter_map(|decorator| decorator.known(db))
|
||||
}
|
||||
|
||||
/// Iterate through the decorators on this class, returning the position of the first one
|
||||
/// that matches the given predicate.
|
||||
pub(super) fn find_decorator_position(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
predicate: impl Fn(Type<'db>) -> bool,
|
||||
) -> Option<usize> {
|
||||
self.decorators(db)
|
||||
.iter()
|
||||
.position(|decorator| predicate(*decorator))
|
||||
}
|
||||
|
||||
/// Iterate through the decorators on this class, returning the index of the first one
|
||||
/// that is either `@dataclass` or `@dataclass(...)`.
|
||||
pub(super) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option<usize> {
|
||||
self.find_decorator_position(db, |ty| match ty {
|
||||
Type::FunctionLiteral(function) => function.is_known(db, KnownFunction::Dataclass),
|
||||
Type::DataclassDecorator(_) => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Is this class final?
|
||||
pub(super) fn is_final(self, db: &'db dyn Db) -> bool {
|
||||
self.known_function_decorators(db)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ use crate::types::{
|
||||
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
|
||||
protocol_class::ProtocolClass,
|
||||
};
|
||||
use crate::types::{KnownInstanceType, MemberLookupPolicy};
|
||||
use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy};
|
||||
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::{
|
||||
@@ -121,6 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&INVALID_METHOD_OVERRIDE);
|
||||
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
|
||||
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
|
||||
registry.register_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -2220,6 +2221,44 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for dataclasses with invalid frozen inheritance:
|
||||
/// - A frozen dataclass cannot inherit from a non-frozen dataclass.
|
||||
/// - A non-frozen dataclass cannot inherit from a frozen dataclass.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python raises a `TypeError` at runtime when either of these inheritance
|
||||
/// patterns occurs.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// from dataclasses import dataclass
|
||||
///
|
||||
/// @dataclass
|
||||
/// class Base:
|
||||
/// x: int
|
||||
///
|
||||
/// @dataclass(frozen=True)
|
||||
/// class Child(Base): # Error raised here
|
||||
/// y: int
|
||||
///
|
||||
/// @dataclass(frozen=True)
|
||||
/// class FrozenBase:
|
||||
/// x: int
|
||||
///
|
||||
/// @dataclass
|
||||
/// class NonFrozenChild(FrozenBase): # Error raised here
|
||||
/// y: int
|
||||
/// ```
|
||||
pub(crate) static INVALID_FROZEN_DATACLASS_SUBCLASS = {
|
||||
summary: "detects dataclasses with invalid frozen/non-frozen subclassing",
|
||||
status: LintStatus::stable("0.0.1-alpha.35"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of type check diagnostics.
|
||||
#[derive(Default, Eq, PartialEq, get_size2::GetSize)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
@@ -4269,6 +4308,94 @@ fn report_unsupported_binary_operation_impl<'a>(
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
class: ClassLiteral<'db>,
|
||||
class_node: &ast::StmtClassDef,
|
||||
base_class: ClassLiteral<'db>,
|
||||
base_class_node: &ast::Expr,
|
||||
base_class_params: DataclassFlags,
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) =
|
||||
context.report_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS, class.header_range(db))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = if base_class_params.is_frozen() {
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic("Non-frozen dataclass cannot inherit from frozen dataclass");
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Non-frozen dataclass `{}` cannot inherit from frozen dataclass `{}`",
|
||||
class.name(db),
|
||||
base_class.name(db)
|
||||
));
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Subclass `{}` is not frozen but base class `{}` is",
|
||||
class.name(db),
|
||||
base_class.name(db)
|
||||
));
|
||||
diagnostic
|
||||
} else {
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic("Frozen dataclass cannot inherit from non-frozen dataclass");
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Frozen dataclass `{}` cannot inherit from non-frozen dataclass `{}`",
|
||||
class.name(db),
|
||||
base_class.name(db)
|
||||
));
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Subclass `{}` is frozen but base class `{}` is not",
|
||||
class.name(db),
|
||||
base_class.name(db)
|
||||
));
|
||||
diagnostic
|
||||
};
|
||||
|
||||
diagnostic.annotate(context.secondary(base_class_node));
|
||||
|
||||
if let Some(position) = class.find_dataclass_decorator_position(db) {
|
||||
diagnostic.annotate(
|
||||
context
|
||||
.secondary(&class_node.decorator_list[position])
|
||||
.message(format_args!("`{}` dataclass parameters", class.name(db))),
|
||||
);
|
||||
}
|
||||
diagnostic.info("This causes the class creation to fail");
|
||||
|
||||
if let Some(decorator_position) = base_class.find_dataclass_decorator_position(db) {
|
||||
let mut sub = SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format_args!("Base class definition"),
|
||||
);
|
||||
sub.annotate(
|
||||
Annotation::primary(base_class.header_span(db))
|
||||
.message(format_args!("`{}` definition", base_class.name(db))),
|
||||
);
|
||||
|
||||
let base_class_file = base_class.file(db);
|
||||
let module = parsed_module(db, base_class_file).load(db);
|
||||
|
||||
let decorator_range = base_class
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_class()
|
||||
.node(&module)
|
||||
.decorator_list[decorator_position]
|
||||
.range();
|
||||
|
||||
sub.annotate(
|
||||
Annotation::secondary(Span::from(base_class_file).with_range(decorator_range)).message(
|
||||
format_args!("`{}` dataclass parameters", base_class.name(db)),
|
||||
),
|
||||
);
|
||||
|
||||
diagnostic.sub(sub);
|
||||
}
|
||||
}
|
||||
|
||||
/// This function receives an unresolved `from foo import bar` import,
|
||||
/// where `foo` can be resolved to a module but that module does not
|
||||
/// have a `bar` member or submodule.
|
||||
|
||||
@@ -2358,7 +2358,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
||||
.fmt_detailed(f)?;
|
||||
f.write_str("'>")
|
||||
} else {
|
||||
f.with_type(ty).write_str("typing.TypeAliasType")
|
||||
f.with_type(ty).write_str("TypeAliasType")
|
||||
}
|
||||
}
|
||||
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
|
||||
@@ -2366,9 +2366,9 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> {
|
||||
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
|
||||
KnownInstanceType::TypeVar(typevar_instance) => {
|
||||
if typevar_instance.kind(self.db).is_paramspec() {
|
||||
f.with_type(ty).write_str("typing.ParamSpec")
|
||||
f.with_type(ty).write_str("ParamSpec")
|
||||
} else {
|
||||
f.with_type(ty).write_str("typing.TypeVar")
|
||||
f.with_type(ty).write_str("TypeVar")
|
||||
}
|
||||
}
|
||||
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
|
||||
|
||||
@@ -28,9 +28,9 @@ use crate::module_resolver::{
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::{
|
||||
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
|
||||
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
|
||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
|
||||
place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
||||
builtins_module_scope, builtins_symbol, class_body_implicit_symbol, explicit_global_symbol,
|
||||
global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol,
|
||||
place, place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
||||
};
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||
@@ -69,15 +69,16 @@ use crate::types::diagnostic::{
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
|
||||
hint_if_stdlib_attribute_exists_on_other_versions,
|
||||
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
|
||||
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
|
||||
report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
|
||||
report_instance_layout_conflict, report_invalid_arguments_to_annotated,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment,
|
||||
report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_invalid_exception_tuple_caught,
|
||||
report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
|
||||
report_invalid_or_unsupported_base, report_invalid_return_type,
|
||||
report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore,
|
||||
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
|
||||
report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases,
|
||||
report_implicit_return_type, report_index_out_of_bounds, report_instance_layout_conflict,
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_exception_caught,
|
||||
report_invalid_exception_cause, report_invalid_exception_raised,
|
||||
report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
|
||||
report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
|
||||
report_invalid_return_type, report_invalid_type_checking_constant,
|
||||
report_named_tuple_field_with_leading_underscore,
|
||||
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
|
||||
report_possibly_missing_attribute, report_possibly_unresolved_reference,
|
||||
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
|
||||
@@ -755,6 +756,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let (base_class_literal, _) = base_class.class_literal(self.db());
|
||||
|
||||
if let (Some(base_params), Some(class_params)) = (
|
||||
base_class_literal.dataclass_params(self.db()),
|
||||
class.dataclass_params(self.db()),
|
||||
) {
|
||||
let base_params = base_params.flags(self.db());
|
||||
let class_is_frozen = class_params.flags(self.db()).is_frozen();
|
||||
|
||||
if base_params.is_frozen() != class_is_frozen {
|
||||
report_bad_frozen_dataclass_inheritance(
|
||||
&self.context,
|
||||
class,
|
||||
class_node,
|
||||
base_class_literal,
|
||||
&class_node.bases()[i],
|
||||
base_params,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (4) Check that the class's MRO is resolvable
|
||||
@@ -9188,6 +9210,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
|
||||
PlaceAndQualifiers::from(Place::Undefined)
|
||||
// If we're in a class body, check for implicit class body symbols first.
|
||||
// These take precedence over globals.
|
||||
.or_fall_back_to(db, || {
|
||||
if scope.node(db).scope_kind().is_class()
|
||||
&& let Some(symbol) = place_expr.as_symbol()
|
||||
{
|
||||
let implicit = class_body_implicit_symbol(db, symbol.name());
|
||||
if implicit.place.is_definitely_bound() {
|
||||
return implicit.map_type(|ty| {
|
||||
self.narrow_place_with_applicable_constraints(
|
||||
place_expr,
|
||||
ty,
|
||||
&constraint_keys,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
Place::Undefined.into()
|
||||
})
|
||||
// No nonlocal binding? Check the module's explicit globals.
|
||||
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
||||
.or_fall_back_to(db, || {
|
||||
|
||||
@@ -222,14 +222,14 @@ fn pep695_type_params() {
|
||||
);
|
||||
};
|
||||
|
||||
check_typevar("T", "typing.TypeVar", None, None, None);
|
||||
check_typevar("U", "typing.TypeVar", Some("A"), None, None);
|
||||
check_typevar("V", "typing.TypeVar", None, Some(&["A", "B"]), None);
|
||||
check_typevar("W", "typing.TypeVar", None, None, Some("A"));
|
||||
check_typevar("X", "typing.TypeVar", Some("A"), None, Some("A1"));
|
||||
check_typevar("T", "TypeVar", None, None, None);
|
||||
check_typevar("U", "TypeVar", Some("A"), None, None);
|
||||
check_typevar("V", "TypeVar", None, Some(&["A", "B"]), None);
|
||||
check_typevar("W", "TypeVar", None, None, Some("A"));
|
||||
check_typevar("X", "TypeVar", Some("A"), None, Some("A1"));
|
||||
|
||||
// a typevar with less than two constraints is treated as unconstrained
|
||||
check_typevar("Y", "typing.TypeVar", None, None, None);
|
||||
check_typevar("Y", "TypeVar", None, None, None);
|
||||
}
|
||||
|
||||
/// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing
|
||||
|
||||
Reference in New Issue
Block a user