Skip to content

Commit 125eaaf

Browse files
authored
[red-knot] inferred type, not Unknown, for undeclared paths (#13400)
After looking at more cases (for example, the case in the added test in this PR), I realized that our previous rule, "if a symbol has any declarations, use only declarations for its public type" is not adequate. Rather than using `Unknown` as fallback if the symbol is not declared in some paths, we need to use the inferred type as fallback in that case. For the paths where the symbol _was_ declared, we know that any bindings must be assignable to the declared type in that path, so this won't change the overall declared type in those paths. But for paths where the symbol wasn't declared, this will give us a better type in place of `Unknown`.
1 parent 7aae809 commit 125eaaf

File tree

2 files changed

+49
-15
lines changed

2 files changed

+49
-15
lines changed

crates/red_knot_python_semantic/src/types.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,21 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
5151
// on inference from bindings.
5252
if use_def.has_public_declarations(symbol) {
5353
let declarations = use_def.public_declarations(symbol);
54+
// If the symbol is undeclared in some paths, include the inferred type in the public type.
55+
let undeclared_ty = if declarations.may_be_undeclared() {
56+
Some(bindings_ty(
57+
db,
58+
use_def.public_bindings(symbol),
59+
use_def
60+
.public_may_be_unbound(symbol)
61+
.then_some(Type::Unknown),
62+
))
63+
} else {
64+
None
65+
};
5466
// Intentionally ignore conflicting declared types; that's not our problem, it's the
5567
// problem of the module we are importing from.
56-
declarations_ty(db, declarations).unwrap_or_else(|(ty, _)| ty)
68+
declarations_ty(db, declarations, undeclared_ty).unwrap_or_else(|(ty, _)| ty)
5769
} else {
5870
bindings_ty(
5971
db,
@@ -173,26 +185,21 @@ type DeclaredTypeResult<'db> = Result<Type<'db>, (Type<'db>, Box<[Type<'db>]>)>;
173185
/// `Ok(declared_type)`. If there are conflicting declarations, returns
174186
/// `Err((union_of_declared_types, conflicting_declared_types))`.
175187
///
176-
/// If undeclared is a possibility, `Unknown` type will be part of the return type (and may
188+
/// If undeclared is a possibility, `undeclared_ty` type will be part of the return type (and may
177189
/// conflict with other declarations.)
178190
///
179191
/// # Panics
180-
/// Will panic if there are no declarations and no possibility of undeclared. This is a logic
181-
/// error, as any symbol with zero live declarations clearly must be undeclared.
192+
/// Will panic if there are no declarations and no `undeclared_ty` is provided. This is a logic
193+
/// error, as any symbol with zero live declarations clearly must be undeclared, and the caller
194+
/// should provide an `undeclared_ty`.
182195
fn declarations_ty<'db>(
183196
db: &'db dyn Db,
184197
declarations: DeclarationsIterator<'_, 'db>,
198+
undeclared_ty: Option<Type<'db>>,
185199
) -> DeclaredTypeResult<'db> {
186-
let may_be_undeclared = declarations.may_be_undeclared();
187200
let decl_types = declarations.map(|declaration| declaration_ty(db, declaration));
188201

189-
let mut all_types = (if may_be_undeclared {
190-
Some(Type::Unknown)
191-
} else {
192-
None
193-
})
194-
.into_iter()
195-
.chain(decl_types);
202+
let mut all_types = undeclared_ty.into_iter().chain(decl_types);
196203

197204
let first = all_types.next().expect(
198205
"declarations_ty must not be called with zero declarations and no may-be-undeclared.",

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,14 @@ impl<'db> TypeInferenceBuilder<'db> {
506506
debug_assert!(binding.is_binding(self.db));
507507
let use_def = self.index.use_def_map(binding.file_scope(self.db));
508508
let declarations = use_def.declarations_at_binding(binding);
509+
let undeclared_ty = if declarations.may_be_undeclared() {
510+
Some(Type::Unknown)
511+
} else {
512+
None
513+
};
509514
let mut bound_ty = ty;
510-
let declared_ty =
511-
declarations_ty(self.db, declarations).unwrap_or_else(|(ty, conflicting)| {
515+
let declared_ty = declarations_ty(self.db, declarations, undeclared_ty).unwrap_or_else(
516+
|(ty, conflicting)| {
512517
// TODO point out the conflicting declarations in the diagnostic?
513518
let symbol_table = self.index.symbol_table(binding.file_scope(self.db));
514519
let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name();
@@ -521,7 +526,8 @@ impl<'db> TypeInferenceBuilder<'db> {
521526
),
522527
);
523528
ty
524-
});
529+
},
530+
);
525531
if !bound_ty.is_assignable_to(self.db, declared_ty) {
526532
self.invalid_assignment_diagnostic(node, declared_ty, bound_ty);
527533
// allow declarations to override inference in case of invalid assignment
@@ -5777,6 +5783,27 @@ mod tests {
57775783
assert_public_ty(&db, "/src/a.py", "f", "Literal[f, f]");
57785784
}
57795785

5786+
#[test]
5787+
fn import_from_conditional_reimport_vs_non_declaration() {
5788+
let mut db = setup_db();
5789+
5790+
db.write_file("/src/a.py", "from b import x").unwrap();
5791+
db.write_dedented(
5792+
"/src/b.py",
5793+
"
5794+
if flag:
5795+
from c import x
5796+
else:
5797+
x = 1
5798+
",
5799+
)
5800+
.unwrap();
5801+
db.write_file("/src/c.pyi", "x: int").unwrap();
5802+
5803+
// TODO this should simplify to just 'int'
5804+
assert_public_ty(&db, "/src/a.py", "x", "int | Literal[1]");
5805+
}
5806+
57805807
// Incremental inference tests
57815808

57825809
fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {

0 commit comments

Comments
 (0)