Compare commits
1 Commits
charlie/su
...
zb/cache-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d83e0dc50b |
64
CLAUDE.md
64
CLAUDE.md
@@ -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
1
Cargo.lock
generated
@@ -3246,6 +3246,7 @@ name = "ruff_memory_usage"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"get-size2",
|
||||
"ordermap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,6 +12,7 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
get-size2 = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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>()
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user