mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:15:12 +00:00
[red-knot] Move intersection type tests to Markdown (#15396)
## Summary
[**Rendered version of the new test
suite**](https://github.com/astral-sh/ruff/blob/david/intersection-type-tests/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md)
Moves most of our existing intersection-types tests to a dedicated
Markdown test suite, extends the test coverage, unifies the notation for
these tests, groups tests into a proper structure, and adds some
explanations for various simplification strategies.
This changeset also:
- Adds a new simplification where `~Never` is removed from
intersections.
- Adds a new simplification where adding `~object` simplifies the whole
intersection to `Never`
- Avoids unnecessary assignment-checks between inferred and declared
type. This was added to this changeset to avoid many false positive
errors in this test suite.
Resolves the task described in this old comment
[here](e01da82a5a..e7e432bca2 (r1819924085)
).
## Test Plan
Running the new Markdown tests
---------
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
b861551b6a
commit
f2c3ddc5ea
4 changed files with 888 additions and 620 deletions
|
@ -321,6 +321,14 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
Type::Never => {
|
||||
// Adding ~Never to an intersection is a no-op.
|
||||
}
|
||||
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
|
||||
// Adding ~object to an intersection results in Never.
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
}
|
||||
ty @ Type::Dynamic(_) => {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
|
@ -386,33 +394,34 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::{KnownClass, Truthiness};
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use test_case::test_case;
|
||||
|
||||
#[test]
|
||||
fn build_union_no_elements() {
|
||||
let db = setup_db();
|
||||
let ty = UnionBuilder::new(&db).build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let empty_union = UnionBuilder::new(&db).build();
|
||||
assert_eq!(empty_union, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single_element() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(ty, t0);
|
||||
let union = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(union, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_two_elements() {
|
||||
let db = setup_db();
|
||||
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
@ -420,499 +429,12 @@ mod tests {
|
|||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.positive(db).into_iter().copied().collect()
|
||||
}
|
||||
|
||||
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.negative(db).into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ta = Type::any();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_empty_intersection_equals_object() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).build();
|
||||
|
||||
assert_eq!(ty, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_positive() {
|
||||
let db = setup_db();
|
||||
let ta = Type::any();
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_positive(i0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_negative() {
|
||||
let db = setup_db();
|
||||
let ta = Type::any();
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = KnownClass::Int.to_instance(&db);
|
||||
// i0 = Any & ~Literal[1]
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
// ta_not_i0 = int & ~(Any & ~Literal[1])
|
||||
// -> int & (~Any | Literal[1])
|
||||
// (~Any is equivalent to Any)
|
||||
// -> (int & Any) | (int & Literal[1])
|
||||
// -> (int & Any) | Literal[1]
|
||||
let ta_not_i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_negative(i0)
|
||||
.build();
|
||||
|
||||
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_any() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::any())
|
||||
.build();
|
||||
assert_eq!(ty, Type::any());
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Never)
|
||||
.add_negative(Type::any())
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_multiple_unknown() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::unknown())
|
||||
.add_positive(Type::unknown())
|
||||
.build();
|
||||
assert_eq!(ty, Type::unknown());
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::unknown())
|
||||
.add_negative(Type::unknown())
|
||||
.build();
|
||||
assert_eq!(ty, Type::unknown());
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::unknown())
|
||||
.add_negative(Type::unknown())
|
||||
.build();
|
||||
assert_eq!(ty, Type::unknown());
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::unknown())
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.add_negative(Type::unknown())
|
||||
.build();
|
||||
assert_eq!(
|
||||
ty,
|
||||
IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::unknown())
|
||||
.add_positive(Type::IntLiteral(0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let ta = Type::any();
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
|
||||
let union = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_positive(u0)
|
||||
.build()
|
||||
.expect_union();
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
|
||||
panic!("expected a union of two intersections");
|
||||
};
|
||||
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
|
||||
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
|
||||
// intersection: A & B
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(intersection.pos_vec(&db), &[a, b]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[]);
|
||||
|
||||
// ~intersection => ~A | ~B
|
||||
let negated_intersection = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Intersection(intersection))
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
// should have as elements ~A and ~B
|
||||
let not_a = a.negate(&db);
|
||||
let not_b = b.negate(&db);
|
||||
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_intersection_negation_distributes_over_union() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
r#"
|
||||
class A: ...
|
||||
class B: ...
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let a = global_symbol(&db, module, "A")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let b = global_symbol(&db, module, "B")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
// a_not_b: A & ~B
|
||||
let a_not_b = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_negative(b)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
assert_eq!(a_not_b.pos_vec(&db), &[a]);
|
||||
assert_eq!(a_not_b.neg_vec(&db), &[b]);
|
||||
|
||||
// let's build
|
||||
// int & ~(A & ~B)
|
||||
// = int & ~(A & ~B)
|
||||
// = int & (~A | B)
|
||||
// = (int & ~A) | (int & B)
|
||||
let t = IntersectionBuilder::new(&db)
|
||||
.add_positive(int)
|
||||
.add_negative(Type::Intersection(a_not_b))
|
||||
.build();
|
||||
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_self_negation() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::none(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_positive(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::none(&db))
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_negative_union_de_morgan() {
|
||||
let db = setup_db();
|
||||
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(Type::IntLiteral(1))
|
||||
.add(Type::IntLiteral(2))
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
|
||||
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::IntLiteral(1))
|
||||
.add_negative(Type::IntLiteral(2))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, s);
|
||||
|
||||
let literal = Type::string_literal(&db, "a");
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t)
|
||||
.add_negative(literal)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(literal)
|
||||
.add_positive(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_negative_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let object = KnownClass::Object.to_instance(&db);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(object)
|
||||
.add_negative(s)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
|
||||
let db = setup_db();
|
||||
|
||||
let s1 = Type::IntLiteral(1);
|
||||
let s2 = Type::IntLiteral(2);
|
||||
let t = KnownClass::Int.to_instance(&db);
|
||||
|
||||
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(s1)
|
||||
.add_negative(s2)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_type_and_positive_subtype() {
|
||||
let db = setup_db();
|
||||
|
||||
let t = KnownClass::Str.to_instance(&db);
|
||||
let s = Type::LiteralString;
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// This should also work in the presence of additional contributions:
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Object.to_instance(&db))
|
||||
.add_negative(t)
|
||||
.add_positive(s)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(s)
|
||||
.add_negative(Type::string_literal(&db, "a"))
|
||||
.add_negative(t)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::none(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t1)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
|
||||
// If there are any negative contributions, they should
|
||||
// be removed too.
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(KnownClass::Str.to_instance(&db))
|
||||
.add_negative(Type::LiteralString)
|
||||
.add_positive(t2)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let t_p = KnownClass::Int.to_instance(&db);
|
||||
let t_n = Type::string_literal(&db, "t_n");
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, t_p);
|
||||
|
||||
let int_literal = Type::IntLiteral(1);
|
||||
let expected = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.build();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t_p)
|
||||
.add_negative(int_literal)
|
||||
.add_negative(t_n)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(t_n)
|
||||
.add_negative(int_literal)
|
||||
.add_positive(t_p)
|
||||
.build();
|
||||
assert_eq!(ty, expected);
|
||||
let intersection = IntersectionBuilder::new(&db).build();
|
||||
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
|
||||
}
|
||||
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
|
@ -957,85 +479,4 @@ mod tests {
|
|||
.build();
|
||||
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
|
||||
}
|
||||
|
||||
#[test_case(Type::any())]
|
||||
#[test_case(Type::unknown())]
|
||||
#[test_case(todo_type!())]
|
||||
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
|
||||
let db = setup_db();
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_positive(ty)
|
||||
.add_negative(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
|
||||
let result = IntersectionBuilder::new(&db)
|
||||
.add_negative(ty)
|
||||
.add_positive(ty)
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_of_two_unions_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let union = UnionBuilder::new(&db).add(a).add(b).build();
|
||||
assert_eq!(union.display(&db).to_string(), "A | B");
|
||||
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
|
||||
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(union)
|
||||
.add_positive(reversed_union)
|
||||
.build();
|
||||
assert_eq!(intersection.display(&db).to_string(), "B | A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_of_two_intersections_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build();
|
||||
let reversed_intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(b)
|
||||
.add_positive(a)
|
||||
.build();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(intersection)
|
||||
.add(reversed_intersection)
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "A & B");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,6 +310,18 @@ enum IntersectionOn {
|
|||
Right,
|
||||
}
|
||||
|
||||
/// A helper to track if we already know that declared and inferred types are the same.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum DeclaredAndInferredType<'db> {
|
||||
/// We know that both the declared and inferred types are the same.
|
||||
AreTheSame(Type<'db>),
|
||||
/// Declared and inferred types might be different, we need to check assignability.
|
||||
MightBeDifferent {
|
||||
declared_ty: Type<'db>,
|
||||
inferred_ty: Type<'db>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Builder to infer all types in a region.
|
||||
///
|
||||
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
|
||||
|
@ -895,20 +907,28 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
definition: Definition<'db>,
|
||||
declared_ty: Type<'db>,
|
||||
inferred_ty: Type<'db>,
|
||||
declared_and_inferred_ty: &DeclaredAndInferredType<'db>,
|
||||
) {
|
||||
debug_assert!(definition.is_binding(self.db()));
|
||||
debug_assert!(definition.is_declaration(self.db()));
|
||||
let inferred_ty = if inferred_ty.is_assignable_to(self.db(), declared_ty) {
|
||||
inferred_ty
|
||||
} else {
|
||||
report_invalid_assignment(&self.context, node, declared_ty, inferred_ty);
|
||||
// if the assignment is invalid, fall back to assuming the annotation is correct
|
||||
declared_ty
|
||||
|
||||
let (declared_ty, inferred_ty) = match declared_and_inferred_ty {
|
||||
DeclaredAndInferredType::AreTheSame(ty) => (ty, ty),
|
||||
DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
} => {
|
||||
if inferred_ty.is_assignable_to(self.db(), *declared_ty) {
|
||||
(declared_ty, inferred_ty)
|
||||
} else {
|
||||
report_invalid_assignment(&self.context, node, *declared_ty, *inferred_ty);
|
||||
// if the assignment is invalid, fall back to assuming the annotation is correct
|
||||
(declared_ty, declared_ty)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.types.declarations.insert(definition, declared_ty);
|
||||
self.types.bindings.insert(definition, inferred_ty);
|
||||
self.types.declarations.insert(definition, *declared_ty);
|
||||
self.types.bindings.insert(definition, *inferred_ty);
|
||||
}
|
||||
|
||||
fn add_unknown_declaration_with_binding(
|
||||
|
@ -916,7 +936,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
node: AnyNodeRef,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
self.add_declaration_with_binding(node, definition, Type::unknown(), Type::unknown());
|
||||
self.add_declaration_with_binding(
|
||||
node,
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(Type::unknown()),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_module(&mut self, module: &ast::ModModule) {
|
||||
|
@ -1097,7 +1121,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
decorator_tys.into_boxed_slice(),
|
||||
));
|
||||
|
||||
self.add_declaration_with_binding(function.into(), definition, function_ty, function_ty);
|
||||
self.add_declaration_with_binding(
|
||||
function.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(function_ty),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_parameters(&mut self, parameters: &ast::Parameters) {
|
||||
|
@ -1188,15 +1216,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.map(|default| self.file_expression_ty(default));
|
||||
if let Some(annotation) = parameter.annotation.as_ref() {
|
||||
let declared_ty = self.file_expression_ty(annotation);
|
||||
let inferred_ty = if let Some(default_ty) = default_ty {
|
||||
let declared_and_inferred_ty = if let Some(default_ty) = default_ty {
|
||||
if default_ty.is_assignable_to(self.db(), declared_ty) {
|
||||
UnionType::from_elements(self.db(), [declared_ty, default_ty])
|
||||
DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty,
|
||||
inferred_ty: UnionType::from_elements(self.db(), [declared_ty, default_ty]),
|
||||
}
|
||||
} else if self.in_stub()
|
||||
&& default
|
||||
.as_ref()
|
||||
.is_some_and(|d| d.is_ellipsis_literal_expr())
|
||||
{
|
||||
declared_ty
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
} else {
|
||||
self.context.report_lint(
|
||||
&INVALID_PARAMETER_DEFAULT,
|
||||
|
@ -1205,16 +1236,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
"Default value of type `{}` is not assignable to annotated parameter type `{}`",
|
||||
default_ty.display(self.db()), declared_ty.display(self.db())),
|
||||
);
|
||||
declared_ty
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
}
|
||||
} else {
|
||||
declared_ty
|
||||
DeclaredAndInferredType::AreTheSame(declared_ty)
|
||||
};
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
&declared_and_inferred_ty,
|
||||
);
|
||||
} else {
|
||||
let ty = if let Some(default_ty) = default_ty {
|
||||
|
@ -1240,7 +1270,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let _annotated_ty = self.file_expression_ty(annotation);
|
||||
// TODO `tuple[annotated_ty, ...]`
|
||||
let ty = KnownClass::Tuple.to_instance(self.db());
|
||||
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
);
|
||||
} else {
|
||||
self.add_binding(
|
||||
parameter.into(),
|
||||
|
@ -1265,7 +1299,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let _annotated_ty = self.file_expression_ty(annotation);
|
||||
// TODO `dict[str, annotated_ty]`
|
||||
let ty = KnownClass::Dict.to_instance(self.db());
|
||||
self.add_declaration_with_binding(parameter.into(), definition, ty, ty);
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
);
|
||||
} else {
|
||||
self.add_binding(
|
||||
parameter.into(),
|
||||
|
@ -1308,7 +1346,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let class = Class::new(self.db(), &name.id, body_scope, maybe_known_class);
|
||||
let class_ty = Type::class_literal(class);
|
||||
|
||||
self.add_declaration_with_binding(class_node.into(), definition, class_ty, class_ty);
|
||||
self.add_declaration_with_binding(
|
||||
class_node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(class_ty),
|
||||
);
|
||||
|
||||
// if there are type parameters, then the keywords and bases are within that scope
|
||||
// and we don't need to run inference here
|
||||
|
@ -1365,8 +1407,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
self.add_declaration_with_binding(
|
||||
type_alias.into(),
|
||||
definition,
|
||||
type_alias_ty,
|
||||
type_alias_ty,
|
||||
&DeclaredAndInferredType::AreTheSame(type_alias_ty),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1718,7 +1759,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
bound_or_constraint,
|
||||
default_ty,
|
||||
)));
|
||||
self.add_declaration_with_binding(node.into(), definition, ty, ty);
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_paramspec_definition(
|
||||
|
@ -1733,7 +1778,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} = node;
|
||||
self.infer_optional_expression(default.as_deref());
|
||||
let pep_695_todo = todo_type!("PEP-695 ParamSpec definition types");
|
||||
self.add_declaration_with_binding(node.into(), definition, pep_695_todo, pep_695_todo);
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_typevartuple_definition(
|
||||
|
@ -1748,7 +1797,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} = node;
|
||||
self.infer_optional_expression(default.as_deref());
|
||||
let pep_695_todo = todo_type!("PEP-695 TypeVarTuple definition types");
|
||||
self.add_declaration_with_binding(node.into(), definition, pep_695_todo, pep_695_todo);
|
||||
self.add_declaration_with_binding(
|
||||
node.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(pep_695_todo),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) {
|
||||
|
@ -2006,13 +2059,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
simple: _,
|
||||
} = assignment;
|
||||
|
||||
let mut annotation_ty = self.infer_annotation_expression(
|
||||
let mut declared_ty = self.infer_annotation_expression(
|
||||
annotation,
|
||||
DeferredExpressionState::from(self.are_all_types_deferred()),
|
||||
);
|
||||
|
||||
// Handle various singletons.
|
||||
if let Type::Instance(InstanceType { class }) = annotation_ty {
|
||||
if let Type::Instance(InstanceType { class }) = declared_ty {
|
||||
if class.is_known(self.db(), KnownClass::SpecialForm) {
|
||||
if let Some(name_expr) = target.as_name_expr() {
|
||||
if let Some(known_instance) = KnownInstanceType::try_from_file_and_name(
|
||||
|
@ -2020,27 +2073,29 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
self.file(),
|
||||
&name_expr.id,
|
||||
) {
|
||||
annotation_ty = Type::KnownInstance(known_instance);
|
||||
declared_ty = Type::KnownInstance(known_instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = value.as_deref() {
|
||||
let value_ty = self.infer_expression(value);
|
||||
let value_ty = if self.in_stub() && value.is_ellipsis_literal_expr() {
|
||||
annotation_ty
|
||||
let inferred_ty = self.infer_expression(value);
|
||||
let inferred_ty = if self.in_stub() && value.is_ellipsis_literal_expr() {
|
||||
declared_ty
|
||||
} else {
|
||||
value_ty
|
||||
inferred_ty
|
||||
};
|
||||
self.add_declaration_with_binding(
|
||||
assignment.into(),
|
||||
definition,
|
||||
annotation_ty,
|
||||
value_ty,
|
||||
&DeclaredAndInferredType::MightBeDifferent {
|
||||
declared_ty,
|
||||
inferred_ty,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
self.add_declaration(assignment.into(), definition, annotation_ty);
|
||||
self.add_declaration(assignment.into(), definition, declared_ty);
|
||||
}
|
||||
|
||||
self.infer_expression(target);
|
||||
|
@ -2294,7 +2349,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
full_module_ty
|
||||
};
|
||||
|
||||
self.add_declaration_with_binding(alias.into(), definition, binding_ty, binding_ty);
|
||||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(binding_ty),
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_import_from_statement(&mut self, import: &ast::StmtImportFrom) {
|
||||
|
@ -2470,7 +2529,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
|
||||
);
|
||||
}
|
||||
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
|
||||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
&DeclaredAndInferredType::AreTheSame(ty),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -2496,8 +2559,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
self.add_declaration_with_binding(
|
||||
alias.into(),
|
||||
definition,
|
||||
submodule_ty,
|
||||
submodule_ty,
|
||||
&DeclaredAndInferredType::AreTheSame(submodule_ty),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue