From 918358aaa6868474ea4ac843f99ded42ce49f682 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 6 Dec 2024 05:19:22 -0500 Subject: [PATCH] Migrate some inference tests to mdtests (#14795) As part of #13696, this PR ports a smallish number of inference tests over to the mdtest framework. --- .../resources/mdtest/assignment/unbound.md | 40 ---- .../resources/mdtest/import/errors.md | 21 ++ .../resources/mdtest/import/invalid_syntax.md | 9 + .../resources/mdtest/literal/ellipsis.md | 7 + .../resources/mdtest/scopes/nonlocal.md | 45 ++++ .../resources/mdtest/scopes/unbound.md | 57 +++++ .../src/types/infer.rs | 199 ------------------ .../red_knot_python_semantic/src/types/mro.rs | 9 - 8 files changed, 139 insertions(+), 248 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md index 4d98329ca6..06be0b4d86 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md @@ -18,43 +18,3 @@ Note: in this particular example, one could argue that the most likely error wou of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of `x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option. - -## Unbound class variable - -Name lookups within a class scope fall back to globals, but lookups of class attributes don't. - -```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 - -class C: - y = x - if flag: - x = 2 - -# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound" -reveal_type(C.x) # revealed: Literal[2] -reveal_type(C.y) # revealed: Literal[1] -``` - -## Possibly unbound in class and global scope - -```py -def bool_instance() -> bool: - return True - -if bool_instance(): - x = "abc" - -class C: - if bool_instance(): - x = 1 - - # error: [possibly-unresolved-reference] - y = x - -reveal_type(C.y) # revealed: Literal[1] | Literal["abc"] -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md b/crates/red_knot_python_semantic/resources/mdtest/import/errors.md index 1ed943cd13..6c30a92021 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/errors.md @@ -55,3 +55,24 @@ from b import x x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]" ``` + +## Import cycle + +```py path=a.py +class A: ... + +reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]] +import b + +class C(b.B): ... + +reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]] +``` + +```py path=b.py +from a import A + +class B(A): ... + +reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md b/crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md new file mode 100644 index 0000000000..cd9ab6358e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/invalid_syntax.md @@ -0,0 +1,9 @@ +# Invalid syntax + +## Missing module name + +```py +from import bar # error: [invalid-syntax] + +reveal_type(bar) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md b/crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md new file mode 100644 index 0000000000..2b7bb7c61d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/ellipsis.md @@ -0,0 +1,7 @@ +# Ellipsis literals + +## Simple + +```py +reveal_type(...) # revealed: EllipsisType | ellipsis +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md new file mode 100644 index 0000000000..7d05d9af6c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -0,0 +1,45 @@ +# Nonlocal references + +## One level up + +```py +def f(): + x = 1 + def g(): + reveal_type(x) # revealed: Literal[1] +``` + +## Two levels up + +```py +def f(): + x = 1 + def g(): + def h(): + reveal_type(x) # revealed: Literal[1] +``` + +## Skips class scope + +```py +def f(): + x = 1 + + class C: + x = 2 + def g(): + reveal_type(x) # revealed: Literal[1] +``` + +## Skips annotation-only assignment + +```py +def f(): + x = 1 + def g(): + # it's pretty weird to have an annotated assignment in a function where the + # name is otherwise not defined; maybe should be an error? + x: int + def h(): + reveal_type(x) # revealed: Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md new file mode 100644 index 0000000000..fc6ee0de10 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md @@ -0,0 +1,57 @@ +# Unbound + +## Unbound class variable + +Name lookups within a class scope fall back to globals, but lookups of class attributes don't. + +```py +def bool_instance() -> bool: + return True + +flag = bool_instance() +x = 1 + +class C: + y = x + if flag: + x = 2 + +# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound" +reveal_type(C.x) # revealed: Literal[2] +reveal_type(C.y) # revealed: Literal[1] +``` + +## Possibly unbound in class and global scope + +```py +def bool_instance() -> bool: + return True + +if bool_instance(): + x = "abc" + +class C: + if bool_instance(): + x = 1 + + # error: [possibly-unresolved-reference] + y = x + +reveal_type(C.y) # revealed: Literal[1] | Literal["abc"] +``` + +## Unbound function local + +An unbound function local that has definitions in the scope does not fall back to globals. + +```py +x = 1 + +def f(): + # error: [unresolved-reference] + # revealed: Unknown + reveal_type(x) + x = 2 + # revealed: Literal[2] + reveal_type(x) +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index afdd60e829..fbf592bd4a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -5123,16 +5123,6 @@ mod tests { assert_diagnostic_messages(diagnostics, expected); } - #[test] - fn from_import_with_no_module_name() -> anyhow::Result<()> { - // This test checks that invalid syntax in a `StmtImportFrom` node - // leads to the type being inferred as `Unknown` - let mut db = setup_db(); - db.write_file("src/foo.py", "from import bar")?; - assert_public_ty(&db, "src/foo.py", "bar", "Unknown"); - Ok(()) - } - #[test] fn resolve_method() -> anyhow::Result<()> { let mut db = setup_db(); @@ -5282,112 +5272,6 @@ mod tests { Ok(()) } - #[test] - fn bytes_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - w = b'red' b'knot' - x = b'hello' - y = b'world' + b'!' - z = b'\\xff\\x00' - ", - )?; - - assert_public_ty(&db, "src/a.py", "w", "Literal[b\"redknot\"]"); - assert_public_ty(&db, "src/a.py", "x", "Literal[b\"hello\"]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[b\"world!\"]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[b\"\\xff\\x00\"]"); - - Ok(()) - } - - #[test] - fn ellipsis_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = ... - ", - )?; - - // TODO: sys.version_info - assert_public_ty(&db, "src/a.py", "x", "EllipsisType | ellipsis"); - - Ok(()) - } - - #[test] - fn import_cycle() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class A: pass - import b - class C(b.B): pass - ", - )?; - db.write_dedented( - "src/b.py", - " - from a import A - class B(A): pass - ", - )?; - - let a = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let c_ty = global_symbol(&db, a, "C").expect_type(); - let c_class = c_ty.expect_class_literal().class; - let mut c_mro = c_class.iter_mro(&db); - let b_ty = c_mro.nth(1).unwrap(); - let b_class = b_ty.expect_class_base(); - assert_eq!(b_class.name(&db), "B"); - let mut b_mro = b_class.iter_mro(&db); - let a_ty = b_mro.nth(1).unwrap(); - let a_class = a_ty.expect_class_base(); - assert_eq!(a_class.name(&db), "A"); - - Ok(()) - } - - /// An unbound function local that has definitions in the scope does not fall back to globals. - #[test] - fn unbound_function_local() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 1 - def f(): - y = x - x = 2 - ", - )?; - - let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let index = semantic_index(&db, file); - let function_scope = index - .child_scopes(FileScopeId::global()) - .next() - .unwrap() - .0 - .to_scope_id(&db, file); - let y_ty = symbol(&db, function_scope, "y").expect_type(); - let x_ty = symbol(&db, function_scope, "x").expect_type(); - - assert_eq!(y_ty.display(&db).to_string(), "Unknown"); - assert_eq!(x_ty.display(&db).to_string(), "Literal[2]"); - - Ok(()) - } - /// A name reference to a never-defined symbol in a function is implicitly a global lookup. #[test] fn implicit_global_in_function() -> anyhow::Result<()> { @@ -5562,89 +5446,6 @@ mod tests { Ok(()) } - #[test] - fn nonlocal_name_reference() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_multi_level() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - def h(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_skips_class_scope() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - class C: - x = 2 - def g(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "C", "g"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_skips_annotation_only_assignment() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - // it's pretty weird to have an annotated assignment in a function where the - // name is otherwise not defined; maybe should be an error? - x: int - def h(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); - - Ok(()) - } - #[test] fn basic_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index d7f4cc34bd..81994fe11c 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -328,15 +328,6 @@ impl<'db> ClassBase<'db> { Display { base: self, db } } - #[cfg(test)] - #[track_caller] - pub(super) fn expect_class_base(self) -> Class<'db> { - match self { - ClassBase::Class(class) => class, - _ => panic!("Expected a `ClassBase::Class()` variant"), - } - } - /// Return a `ClassBase` representing the class `builtins.object` fn object(db: &'db dyn Db) -> Self { KnownClass::Object