[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

@ -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]
``` ```

View file

@ -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
```

View file

@ -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
``` ```

View file

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