mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00
[red-knot] don't include Unknown in the type for a conditionally-defined import (#13563)
## Summary Fixes the bug described in #13514 where an unbound public type defaulted to the type or `Unknown`, whereas it should only be the type if unbound. ## Test Plan Added a new test case --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
2095ea8372
commit
ed4a0b34ba
10 changed files with 126 additions and 91 deletions
|
@ -37,6 +37,13 @@ fn core_module_symbol_ty<'db>(
|
|||
) -> Type<'db> {
|
||||
resolve_module(db, &core_module.name())
|
||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
||||
.map(|ty| {
|
||||
if ty.is_unbound() {
|
||||
ty
|
||||
} else {
|
||||
ty.replace_unbound_with(db, Type::Never)
|
||||
}
|
||||
})
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
|
|||
use_def.public_bindings(symbol),
|
||||
use_def
|
||||
.public_may_be_unbound(symbol)
|
||||
.then_some(Type::Unknown),
|
||||
.then_some(Type::Unbound),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
|
@ -79,7 +79,7 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
|
|||
}
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
|
||||
/// Shorthand for `symbol_ty_by_id` that takes a symbol name instead of an ID.
|
||||
fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
|
||||
let table = symbol_table(db, scope);
|
||||
table
|
||||
|
@ -381,7 +381,7 @@ impl<'db> Type<'db> {
|
|||
Type::Union(union) => {
|
||||
union.map(db, |element| element.replace_unbound_with(db, replacement))
|
||||
}
|
||||
ty => *ty,
|
||||
_ => *self,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -444,6 +444,9 @@ impl<'db> Type<'db> {
|
|||
///
|
||||
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
(Type::Unknown | Type::Any | Type::Todo, _) => true,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => true,
|
||||
|
@ -1426,7 +1429,8 @@ impl<'db> ClassType<'db> {
|
|||
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
let member = self.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
// TODO diagnostic if maybe unbound?
|
||||
return member.replace_unbound_with(db, Type::Never);
|
||||
}
|
||||
|
||||
self.inherited_class_member(db, name)
|
||||
|
@ -1625,6 +1629,7 @@ mod tests {
|
|||
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
|
||||
fn is_assignable_to(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
|
||||
|
|
|
@ -1699,10 +1699,30 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.ok_or(ModuleNameResolutionError::InvalidSyntax)
|
||||
};
|
||||
|
||||
let module_ty = match module_name {
|
||||
Ok(name) => {
|
||||
if let Some(ty) = self.module_ty_from_name(&name) {
|
||||
ty
|
||||
let ty = match module_name {
|
||||
Ok(module_name) => {
|
||||
if let Some(module_ty) = self.module_ty_from_name(&module_name) {
|
||||
let ast::Alias {
|
||||
range: _,
|
||||
name,
|
||||
asname: _,
|
||||
} = alias;
|
||||
|
||||
let member_ty = module_ty.member(self.db, &ast::name::Name::new(&name.id));
|
||||
|
||||
if member_ty.is_unbound() {
|
||||
self.add_diagnostic(
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
|
||||
Type::Unknown
|
||||
} else {
|
||||
// For possibly-unbound names, just eliminate Unbound from the type; we
|
||||
// must be in a bound path. TODO diagnostic for maybe-unbound import?
|
||||
member_ty.replace_unbound_with(self.db, Type::Never)
|
||||
}
|
||||
} else {
|
||||
self.unresolved_module_diagnostic(import_from, *level, module);
|
||||
Type::Unknown
|
||||
|
@ -1732,34 +1752,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
};
|
||||
|
||||
let ast::Alias {
|
||||
range: _,
|
||||
name,
|
||||
asname: _,
|
||||
} = alias;
|
||||
|
||||
let member_ty = module_ty.member(self.db, &ast::name::Name::new(&name.id));
|
||||
|
||||
// TODO: What if it's a union where one of the elements is `Unbound`?
|
||||
if member_ty.is_unbound() {
|
||||
self.add_diagnostic(
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!(
|
||||
"Module `{}{}` has no member `{name}`",
|
||||
".".repeat(*level as usize),
|
||||
module.unwrap_or_default()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// If a symbol is unbound in the module the symbol was originally defined in,
|
||||
// when we're trying to import the symbol from that module into "our" module,
|
||||
// the runtime error will occur immediately (rather than when the symbol is *used*,
|
||||
// as would be the case for a symbol with type `Unbound`), so it's appropriate to
|
||||
// think of the type of the imported symbol as `Unknown` rather than `Unbound`
|
||||
let ty = member_ty.replace_unbound_with(self.db, Type::Unknown);
|
||||
|
||||
self.add_declaration_with_binding(alias.into(), definition, ty, ty);
|
||||
}
|
||||
|
||||
|
@ -2368,6 +2360,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
return symbol_ty(self.db, enclosing_scope_id, name);
|
||||
}
|
||||
}
|
||||
|
||||
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
|
||||
// already is module globals.
|
||||
let ty = if file_scope_id.is_global() {
|
||||
|
@ -2375,6 +2368,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} else {
|
||||
global_symbol_ty(self.db, self.file, name)
|
||||
};
|
||||
|
||||
// Fallback to builtins (without infinite recursion if we're already in builtins.)
|
||||
if ty.may_be_unbound(self.db) && Some(self.scope()) != builtins_module_scope(self.db) {
|
||||
let mut builtin_ty = builtins_symbol_ty(self.db, name);
|
||||
|
@ -3877,7 +3871,7 @@ mod tests {
|
|||
)?;
|
||||
|
||||
// TODO: sys.version_info, and need to understand @final and @type_check_only
|
||||
assert_public_ty(&db, "src/a.py", "x", "Unknown | EllipsisType");
|
||||
assert_public_ty(&db, "src/a.py", "x", "EllipsisType | Unknown");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -3994,38 +3988,6 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditionally_global_or_builtin() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"/src/a.py",
|
||||
"
|
||||
if flag:
|
||||
copyright = 1
|
||||
def f():
|
||||
y = copyright
|
||||
",
|
||||
)?;
|
||||
|
||||
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_ty(&db, function_scope, "y");
|
||||
|
||||
assert_eq!(
|
||||
y_ty.display(&db).to_string(),
|
||||
"Literal[copyright] | Literal[1]"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_inference() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue