Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie Blue
d83e0dc50b [ty] Cache ClassType::nearest_disjoint_base 2026-01-03 08:03:07 -06:00
16 changed files with 57 additions and 225 deletions

View File

@@ -1,64 +0,0 @@
# Ruff Repository
This repository contains both Ruff (a Python linter and formatter) and ty (a Python type checker). The crates follow a naming convention: `ruff_*` for Ruff-specific code and `ty_*` for ty-specific code. ty reuses several Ruff crates, including the Python parser (`ruff_python_parser`) and AST definitions (`ruff_python_ast`).
## Running Tests
Run all tests (using `nextest` for faster execution):
```sh
cargo nextest run
```
Run tests for a specific crate:
```sh
cargo nextest run -p ty_python_semantic
```
Run a specific mdtest (use a substring of the test name):
```sh
MDTEST_TEST_FILTER="<filter>" cargo nextest run -p ty_python_semantic mdtest
```
Update snapshots after running tests:
```sh
cargo insta accept
```
## Running Clippy
```sh
cargo clippy --workspace --all-targets --all-features -- -D warnings
```
## Running Debug Builds
Use debug builds (not `--release`) when developing, as release builds lack debug assertions and have slower compile times.
Run Ruff:
```sh
cargo run --bin ruff -- check path/to/file.py
```
Run ty:
```sh
cargo run --bin ty -- check path/to/file.py
```
## Pull Requests
When working on ty, PR titles should start with `[ty]` and be tagged with the `ty` GitHub label.
## Development Guidelines
- All changes must be tested. If you're not testing your changes, you're not done.
- Get your tests to pass. If you didn't run the tests, your code does not work.
- Follow existing code style. Check neighboring files for patterns.
- Always run `uvx pre-commit run -a` at the end of a task.
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.

1
Cargo.lock generated
View File

@@ -3246,6 +3246,7 @@ name = "ruff_memory_usage"
version = "0.0.0"
dependencies = [
"get-size2",
"ordermap",
]
[[package]]

View File

@@ -93,7 +93,6 @@ get-size2 = { version = "0.7.3", features = [
"smallvec",
"hashbrown",
"compact-str",
"ordermap"
] }
getrandom = { version = "0.3.1" }
glob = { version = "0.3.1" }

View File

@@ -557,60 +557,6 @@ fn benchmark_many_enum_members(criterion: &mut Criterion) {
});
}
fn benchmark_many_enum_members_2(criterion: &mut Criterion) {
const NUM_ENUM_MEMBERS: usize = 48;
setup_rayon();
let mut code = "\
from enum import Enum
from typing_extensions import assert_never
class E(Enum):
"
.to_string();
for i in 0..NUM_ENUM_MEMBERS {
writeln!(&mut code, " m{i} = {i}").ok();
}
code.push_str(
"
def method(self):
match self:",
);
for i in 0..NUM_ENUM_MEMBERS {
write!(
&mut code,
"
case E.m{i}:
pass"
)
.ok();
}
write!(
&mut code,
"
case _:
assert_never(self)"
)
.ok();
criterion.bench_function("ty_micro[many_enum_members_2]", |b| {
b.iter_batched_ref(
|| setup_micro_case(&code),
|case| {
let Case { db, .. } = case;
let result = db.check();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
struct ProjectBenchmark<'a> {
project: InstalledProject<'a>,
fs: MemoryFileSystem,
@@ -771,7 +717,6 @@ criterion_group!(
benchmark_complex_constrained_attributes_2,
benchmark_complex_constrained_attributes_3,
benchmark_many_enum_members,
benchmark_many_enum_members_2,
);
criterion_group!(project, anyio, attrs, hydra, datetype);
criterion_main!(check_file, micro, project);

View File

@@ -12,6 +12,7 @@ license = { workspace = true }
[dependencies]
get-size2 = { workspace = true }
ordermap = { workspace = true }
[lints]
workspace = true

View File

@@ -1,6 +1,7 @@
use std::cell::RefCell;
use get_size2::{GetSize, StandardTracker};
use ordermap::{OrderMap, OrderSet};
thread_local! {
pub static TRACKER: RefCell<Option<StandardTracker>>= const { RefCell::new(None) };
@@ -41,3 +42,16 @@ pub fn heap_size<T: GetSize>(value: &T) -> usize {
}
})
}
/// An implementation of [`GetSize::get_heap_size`] for [`OrderSet`].
pub fn order_set_heap_size<T: GetSize, S>(set: &OrderSet<T, S>) -> usize {
(set.capacity() * T::get_stack_size()) + set.iter().map(heap_size).sum::<usize>()
}
/// An implementation of [`GetSize::get_heap_size`] for [`OrderMap`].
pub fn order_map_heap_size<K: GetSize, V: GetSize, S>(map: &OrderMap<K, V, S>) -> usize {
(map.capacity() * (K::get_stack_size() + V::get_stack_size()))
+ (map.iter())
.map(|(k, v)| heap_size(k) + heap_size(v))
.sum::<usize>()
}

View File

@@ -234,38 +234,3 @@ def takes_no_argument() -> str:
@takes_no_argument
def g(x): ...
```
## Class decorators
Class decorator calls are validated, emitting diagnostics for invalid arguments:
```py
def takes_int(x: int) -> int:
return x
# error: [invalid-argument-type]
@takes_int
class Foo: ...
```
Using `None` as a decorator is an error:
```py
# error: [call-non-callable]
@None
class Bar: ...
```
A decorator can enforce type constraints on the class being decorated:
```py
def decorator(cls: type[int]) -> type[int]:
return cls
# error: [invalid-argument-type]
@decorator
class Baz: ...
# TODO: the revealed type should ideally be `type[int]` (the decorator's return type)
reveal_type(Baz) # revealed: <class 'Baz'>
```

View File

@@ -172,25 +172,6 @@ def _(x: X, y: tuple[Literal[1], Literal[3]]):
reveal_type(x < y) # revealed: Literal[True]
```
## In subscripts
Subscript operations should work through type aliases.
```py
class C:
def __getitem__(self, index: int) -> int:
return 1
class D:
def __getitem__(self, index: int) -> int:
return 1
type CD = C | D
def _(x: CD):
reveal_type(x[1]) # revealed: int
```
## `TypeAliasType` properties
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type

View File

@@ -7267,7 +7267,10 @@ impl<'db> Type<'db> {
}
(Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => {
let callable = UnionType::from_elements(db, [new_method, init_method]);
let callable = UnionBuilder::new(db)
.add(*new_method)
.add(*init_method)
.build();
let new_method_bindings = new_method
.bindings(db)
@@ -10755,7 +10758,11 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
impl<'db> TypeVarConstraints<'db> {
fn as_type(self, db: &'db dyn Db) -> Type<'db> {
UnionType::from_elements(db, self.elements(db))
let mut builder = UnionBuilder::new(db);
for ty in self.elements(db) {
builder = builder.add(*ty);
}
builder.build()
}
fn to_instance(self, db: &'db dyn Db) -> Option<TypeVarConstraints<'db>> {
@@ -14364,7 +14371,7 @@ impl KnownUnion {
}
}
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[salsa::interned(debug, heap_size=IntersectionType::heap_size)]
pub struct IntersectionType<'db> {
/// The intersection type includes only values in all of these types.
#[returns(ref)]
@@ -14653,6 +14660,11 @@ impl<'db> IntersectionType<'db> {
pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool {
self.positive(db).is_empty() && self.negative(db).len() == 1
}
fn heap_size((positive, negative): &(FxOrderSet<Type<'db>>, FxOrderSet<Type<'db>>)) -> usize {
ruff_memory_usage::order_set_heap_size(positive)
+ ruff_memory_usage::order_set_heap_size(negative)
}
}
/// # Ordering

View File

@@ -715,6 +715,7 @@ impl<'db> ClassType<'db> {
/// Return the [`DisjointBase`] that appears first in the MRO of this class.
///
/// Returns `None` if this class does not have any disjoint bases in its MRO.
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
pub(super) fn nearest_disjoint_base(self, db: &'db dyn Db) -> Option<DisjointBase<'db>> {
self.iter_mro(db)
.filter_map(ClassBase::into_class)
@@ -4233,7 +4234,7 @@ impl InheritanceCycle {
/// `TypeError`s resulting from class definitions.
///
/// [PEP 800]: https://peps.python.org/pep-0800/
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, get_size2::GetSize, salsa::Update)]
pub(super) struct DisjointBase<'db> {
pub(super) class: ClassLiteral<'db>,
pub(super) kind: DisjointBaseKind,
@@ -4270,7 +4271,7 @@ impl<'db> DisjointBase<'db> {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
pub(super) enum DisjointBaseKind {
/// We know the class is a disjoint base because it's either hardcoded in ty
/// or has the `@disjoint_base` decorator.

View File

@@ -202,7 +202,7 @@ impl<'a, 'db> InferableTypeVars<'a, 'db> {
/// # Ordering
/// Ordering is based on the context's salsa-assigned id and not on its values.
/// The id may change between runs, or when the context was garbage collected and recreated.
#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)]
#[salsa::interned(debug, constructor=new_internal, heap_size=GenericContext::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct GenericContext<'db> {
#[returns(ref)]
@@ -689,6 +689,12 @@ impl<'db> GenericContext<'db> {
Self::from_typevar_instances(db, variables)
}
fn heap_size(
(variables,): &(FxOrderMap<BoundTypeVarIdentity<'db>, BoundTypeVarInstance<'db>>,),
) -> usize {
ruff_memory_usage::order_map_heap_size(variables)
}
}
fn inferable_typevars_cycle_initial<'db>(

View File

@@ -2797,8 +2797,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
body: _,
} = class_node;
let mut decorator_types_and_nodes: Vec<(Type<'db>, &ast::Decorator)> =
Vec::with_capacity(decorator_list.len());
let mut deprecated = None;
let mut type_check_only = false;
let mut dataclass_params = None;
@@ -2833,20 +2831,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
// Skip identity decorators to avoid salsa cycles on typeshed.
if decorator_ty.as_function_literal().is_some_and(|function| {
matches!(
function.known(self.db()),
Some(
KnownFunction::Final
| KnownFunction::DisjointBase
| KnownFunction::RuntimeCheckable
)
)
}) {
continue;
}
if let Type::FunctionLiteral(f) = decorator_ty {
// We do not yet detect or flag `@dataclass_transform` applied to more than one
// overload, or an overload and the implementation both. Nevertheless, this is not
@@ -2868,8 +2852,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
dataclass_transformer_params = Some(params);
continue;
}
decorator_types_and_nodes.push((decorator_ty, decorator));
}
let body_scope = self
@@ -2886,7 +2868,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
};
let inferred_ty = match (maybe_known_class, &*name.id) {
let ty = match (maybe_known_class, &*name.id) {
(None, "NamedTuple") if in_typing_module() => {
Type::SpecialForm(SpecialFormType::NamedTuple)
}
@@ -2903,19 +2885,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)),
};
// Validate decorator calls (but don't use return types yet).
for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() {
if let Err(CallError(_, bindings)) =
decorator_ty.try_call(self.db(), &CallArguments::positional([inferred_ty]))
{
bindings.report_diagnostics(&self.context, (*decorator_node).into());
}
}
self.add_declaration_with_binding(
class_node.into(),
definition,
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
&DeclaredAndInferredType::are_the_same_type(ty),
);
// if there are type parameters, then the keywords and bases are within that scope
@@ -12368,13 +12341,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(todo_type!("Subscript expressions on intersections"))
}
(Type::TypeAlias(alias), _) => Some(self.infer_subscript_expression_types(
subscript,
alias.value_type(db),
slice_ty,
expr_context,
)),
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
(Type::NominalInstance(nominal), Type::IntLiteral(i64_int)) => nominal
.tuple_spec(db)

View File

@@ -1083,9 +1083,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
&mut self.inner_expression_inference_state,
InnerExpressionInferenceState::Get,
);
let union = union.map(self.db(), |element| {
self.infer_subscript_type_expression(subscript, *element)
});
let union = union
.elements(self.db())
.iter()
.fold(UnionBuilder::new(self.db()), |builder, elem| {
builder.add(self.infer_subscript_type_expression(subscript, *elem))
})
.build();
self.inner_expression_inference_state = previous_slice_inference_state;
union
}

View File

@@ -926,7 +926,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
.build();
// Keep order: first literal complement, then broader arms.
let result = UnionType::from_elements(self.db, [narrowed_single, rest_union]);
let result = UnionBuilder::new(self.db)
.add(narrowed_single)
.add(rest_union)
.build();
Some(result)
} else {
None

View File

@@ -1,7 +1,5 @@
use rustc_hash::FxHashSet;
use crate::{
Db,
Db, FxIndexSet,
types::{
BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, GenericAlias,
IntersectionType, KnownBoundMethodType, KnownInstanceType, NominalInstanceType,
@@ -279,7 +277,7 @@ pub(crate) fn walk_type_with_recursion_guard<'db>(
}
#[derive(Default, Debug)]
pub(crate) struct TypeCollector<'db>(RefCell<FxHashSet<Type<'db>>>);
pub(crate) struct TypeCollector<'db>(RefCell<FxIndexSet<Type<'db>>>);
impl<'db> TypeCollector<'db> {
pub(crate) fn type_was_already_seen(&self, ty: Type<'db>) -> bool {

View File

@@ -18,7 +18,7 @@
set -eu
docstring_adder="git+https://github.com/astral-sh/docstring-adder.git@e98a04941d5a6b8b9240e40392de15990b8cb8be"
docstring_adder="git+https://github.com/astral-sh/docstring-adder.git@1a0fb336fdc85014b22daeb34c862b695aef07d4"
stdlib_path="./crates/ty_vendored/vendor/typeshed/stdlib"
for python_version in 3.14 3.13 3.12 3.11 3.10 3.9