[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:
David Peter 2024-11-11 20:48:49 +01:00 committed by GitHub
parent 3bef23669f
commit f1f3bd1cd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 15 deletions

View file

@ -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"]);