mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-01 09:22:19 +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
|
@ -31,7 +31,6 @@ use std::num::NonZeroU32;
|
|||
use itertools::Itertools;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
|
||||
use rustc_hash::FxHashMap;
|
||||
use salsa;
|
||||
|
@ -2796,10 +2795,35 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} = attribute;
|
||||
|
||||
let value_ty = self.infer_expression(value);
|
||||
value_ty
|
||||
.member(self.db, &Name::new(&attr.id))
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Unknown)
|
||||
match value_ty.member(self.db, &attr.id) {
|
||||
Symbol::Type(member_ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
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> {
|
||||
|
@ -4734,13 +4758,12 @@ mod tests {
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_scope_ty(
|
||||
db: &TestDb,
|
||||
fn get_symbol<'db>(
|
||||
db: &'db TestDb,
|
||||
file_name: &str,
|
||||
scopes: &[&str],
|
||||
symbol_name: &str,
|
||||
expected: &str,
|
||||
) {
|
||||
) -> Symbol<'db> {
|
||||
let file = system_path_to_file(db, file_name).expect("file to exist");
|
||||
let index = semantic_index(db, file);
|
||||
let mut file_scope_id = FileScopeId::global();
|
||||
|
@ -4755,9 +4778,18 @@ mod tests {
|
|||
assert_eq!(scope.name(db), *expected_scope_name);
|
||||
}
|
||||
|
||||
let ty = symbol(db, scope, symbol_name)
|
||||
.ignore_possibly_unbound()
|
||||
.unwrap_or(Type::Unknown);
|
||||
symbol(db, scope, symbol_name)
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
|
@ -5437,9 +5469,10 @@ mod tests {
|
|||
|
||||
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_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`)
|
||||
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue