mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
[red-knot] Review remaining 'possibly unbound' call sites (#14284)
## Summary - Emit diagnostics when looking up (possibly) unbound attributes - More explicit test assertions for unbound symbols - Review remaining call sites of `Symbol::ignore_possibly_unbound`. Most of them are something like `builtins_symbol(self.db, "Ellipsis").ignore_possibly_unbound().unwrap_or(Type::Unknown)` which look okay to me, unless we want to emit additional diagnostics. There is one additional case in enum literal handling, which has a TODO comment anyway. part of #14022 ## Test Plan New MD tests for (possibly) unbound attributes.
This commit is contained in:
parent
3bef23669f
commit
f1f3bd1cd3
4 changed files with 79 additions and 15 deletions
|
@ -35,6 +35,7 @@ class C:
|
||||||
if flag:
|
if flag:
|
||||||
x = 2
|
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.x) # revealed: Literal[2]
|
||||||
reveal_type(C.y) # revealed: Literal[1]
|
reveal_type(C.y) # revealed: Literal[1]
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Attribute access
|
||||||
|
|
||||||
|
## Boundness
|
||||||
|
|
||||||
|
```py
|
||||||
|
def flag() -> bool: ...
|
||||||
|
|
||||||
|
class A:
|
||||||
|
always_bound = 1
|
||||||
|
|
||||||
|
if flag():
|
||||||
|
union = 1
|
||||||
|
else:
|
||||||
|
union = "abc"
|
||||||
|
|
||||||
|
if flag():
|
||||||
|
possibly_unbound = "abc"
|
||||||
|
|
||||||
|
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||||
|
|
||||||
|
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||||
|
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||||
|
reveal_type(A.non_existent) # revealed: Unknown
|
||||||
|
```
|
|
@ -74,6 +74,7 @@ we're dealing with:
|
||||||
```py path=__getattr__.py
|
```py path=__getattr__.py
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
# error: [unresolved-attribute]
|
||||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ use std::num::NonZeroU32;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
use ruff_python_ast::name::Name;
|
|
||||||
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
|
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use salsa;
|
use salsa;
|
||||||
|
@ -2796,10 +2795,35 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
} = attribute;
|
} = attribute;
|
||||||
|
|
||||||
let value_ty = self.infer_expression(value);
|
let value_ty = self.infer_expression(value);
|
||||||
value_ty
|
match value_ty.member(self.db, &attr.id) {
|
||||||
.member(self.db, &Name::new(&attr.id))
|
Symbol::Type(member_ty, boundness) => {
|
||||||
.ignore_possibly_unbound()
|
if boundness == Boundness::PossiblyUnbound {
|
||||||
.unwrap_or(Type::Unknown)
|
self.diagnostics.add(
|
||||||
|
attribute.into(),
|
||||||
|
"possibly-unbound-attribute",
|
||||||
|
format_args!(
|
||||||
|
"Attribute `{}` on type `{}` is possibly unbound",
|
||||||
|
attr.id,
|
||||||
|
value_ty.display(self.db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
member_ty
|
||||||
|
}
|
||||||
|
Symbol::Unbound => {
|
||||||
|
self.diagnostics.add(
|
||||||
|
attribute.into(),
|
||||||
|
"unresolved-attribute",
|
||||||
|
format_args!(
|
||||||
|
"Type `{}` has no attribute `{}`",
|
||||||
|
value_ty.display(self.db),
|
||||||
|
attr.id
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Type::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||||
|
@ -4734,13 +4758,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_scope_ty(
|
fn get_symbol<'db>(
|
||||||
db: &TestDb,
|
db: &'db TestDb,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
scopes: &[&str],
|
scopes: &[&str],
|
||||||
symbol_name: &str,
|
symbol_name: &str,
|
||||||
expected: &str,
|
) -> Symbol<'db> {
|
||||||
) {
|
|
||||||
let file = system_path_to_file(db, file_name).expect("file to exist");
|
let file = system_path_to_file(db, file_name).expect("file to exist");
|
||||||
let index = semantic_index(db, file);
|
let index = semantic_index(db, file);
|
||||||
let mut file_scope_id = FileScopeId::global();
|
let mut file_scope_id = FileScopeId::global();
|
||||||
|
@ -4755,9 +4778,18 @@ mod tests {
|
||||||
assert_eq!(scope.name(db), *expected_scope_name);
|
assert_eq!(scope.name(db), *expected_scope_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ty = symbol(db, scope, symbol_name)
|
symbol(db, scope, symbol_name)
|
||||||
.ignore_possibly_unbound()
|
}
|
||||||
.unwrap_or(Type::Unknown);
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_scope_ty(
|
||||||
|
db: &TestDb,
|
||||||
|
file_name: &str,
|
||||||
|
scopes: &[&str],
|
||||||
|
symbol_name: &str,
|
||||||
|
expected: &str,
|
||||||
|
) {
|
||||||
|
let ty = get_symbol(db, file_name, scopes, symbol_name).expect_type();
|
||||||
assert_eq!(ty.display(db).to_string(), expected);
|
assert_eq!(ty.display(db).to_string(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5437,9 +5469,10 @@ mod tests {
|
||||||
|
|
||||||
db.write_dedented("src/a.py", "[z for z in x]")?;
|
db.write_dedented("src/a.py", "[z for z in x]")?;
|
||||||
|
|
||||||
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "x", "Unknown");
|
let x = get_symbol(&db, "src/a.py", &["<listcomp>"], "x");
|
||||||
|
assert!(x.is_unbound());
|
||||||
|
|
||||||
// Iterating over an `Unbound` yields `Unknown`:
|
// Iterating over an unbound iterable yields `Unknown`:
|
||||||
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "z", "Unknown");
|
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "z", "Unknown");
|
||||||
|
|
||||||
assert_file_diagnostics(&db, "src/a.py", &["Name `x` used when not defined"]);
|
assert_file_diagnostics(&db, "src/a.py", &["Name `x` used when not defined"]);
|
||||||
|
@ -5573,7 +5606,8 @@ mod tests {
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unknown");
|
let z = get_symbol(&db, "src/a.py", &["foo", "<listcomp>"], "z");
|
||||||
|
assert!(z.is_unbound());
|
||||||
|
|
||||||
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
|
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
|
||||||
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
|
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue