-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/unknown kinds #13500
Feat/unknown kinds #13500
Changes from all commits
53ff3df
60d8aaf
2d0af15
740f24c
e66ff61
ecc38c6
6e7003f
ba8962b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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::Unknown(UnknownTypeKind::TypeError)), | ||
)) | ||
} else { | ||
None | ||
|
@@ -239,7 +239,7 @@ pub enum Type<'db> { | |
Never, | ||
/// unknown type (either no annotation, or some kind of type error) | ||
/// equivalent to Any, or possibly to object in strict mode | ||
Unknown, | ||
Unknown(UnknownTypeKind), | ||
/// name does not exist or is not bound to any value (this represents an error, but with some | ||
/// leniency options it could be silently resolved to Unknown in some cases) | ||
Unbound, | ||
|
@@ -369,14 +369,24 @@ impl<'db> Type<'db> { | |
} | ||
} | ||
|
||
/// Apply a function to the type (if it is a non-union); or, | ||
/// if it is a union, apply the function to each element of the union | ||
/// and build a new union from the result | ||
#[must_use] | ||
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> { | ||
pub fn recursive_transform( | ||
self, | ||
db: &'db dyn Db, | ||
mut transform_fn: impl FnMut(Type<'db>) -> Type<'db>, | ||
) -> Self { | ||
match self { | ||
Type::Unbound => replacement, | ||
Type::Union(union) => { | ||
union.map(db, |element| element.replace_unbound_with(db, replacement)) | ||
} | ||
ty => *ty, | ||
Type::Union(union) => union | ||
.elements(db) | ||
.iter() | ||
.fold(UnionBuilder::new(db), |builder, ty| { | ||
builder.add(transform_fn(*ty)) | ||
}) | ||
.build(), | ||
ty => transform_fn(ty), | ||
} | ||
} | ||
|
||
|
@@ -398,8 +408,8 @@ impl<'db> Type<'db> { | |
return true; | ||
} | ||
match (self, target) { | ||
(Type::Unknown | Type::Any, _) => false, | ||
(_, Type::Unknown | Type::Any) => false, | ||
(Type::Unknown(_) | Type::Any, _) => false, | ||
(_, Type::Unknown(_) | Type::Any) => false, | ||
(Type::Never, _) => true, | ||
(_, Type::Never) => false, | ||
(Type::IntLiteral(_), Type::Instance(class)) | ||
|
@@ -434,8 +444,8 @@ 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 { | ||
match (self, target) { | ||
(Type::Unknown | Type::Any, _) => true, | ||
(_, Type::Unknown | Type::Any) => true, | ||
(Type::Unknown(_) | Type::Any, _) => true, | ||
(_, Type::Unknown(_) | Type::Any) => true, | ||
(ty, Type::Union(union)) => union | ||
.elements(db) | ||
.iter() | ||
|
@@ -471,52 +481,52 @@ impl<'db> Type<'db> { | |
Type::Any => Type::Any, | ||
Type::Never => { | ||
// TODO: attribute lookup on Never type | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::Unknown => Type::Unknown, | ||
Type::Unknown(kind) => Type::Unknown(*kind), | ||
Type::Unbound => Type::Unbound, | ||
Type::None => { | ||
// TODO: attribute lookup on None type | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::Function(_) | Type::RevealTypeFunction(_) => { | ||
// TODO: attribute lookup on function type | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::Module(file) => global_symbol_ty(db, *file, name), | ||
Type::Class(class) => class.class_member(db, name), | ||
Type::Instance(_) => { | ||
// TODO MRO? get_own_instance_member, get_instance_member | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::Union(union) => union.map(db, |element| element.member(db, name)), | ||
Type::Intersection(_) => { | ||
// TODO perform the get_member on each type in the intersection | ||
// TODO return the intersection of those results | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::IntLiteral(_) => { | ||
// TODO raise error | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::BooleanLiteral(_) => Type::Unknown, | ||
Type::BooleanLiteral(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation), | ||
Type::StringLiteral(_) => { | ||
// TODO defer to `typing.LiteralString`/`builtins.str` methods | ||
// from typeshed's stubs | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::LiteralString => { | ||
// TODO defer to `typing.LiteralString`/`builtins.str` methods | ||
// from typeshed's stubs | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::BytesLiteral(_) => { | ||
// TODO defer to Type::Instance(<bytes from typeshed>).member | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
Type::Tuple(_) => { | ||
// TODO: implement tuple methods | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} | ||
} | ||
} | ||
|
@@ -531,19 +541,23 @@ impl<'db> Type<'db> { | |
Type::Function(function_type) => CallOutcome::callable(function_type.return_type(db)), | ||
Type::RevealTypeFunction(function_type) => CallOutcome::revealed( | ||
function_type.return_type(db), | ||
*arg_types.first().unwrap_or(&Type::Unknown), | ||
*arg_types | ||
.first() | ||
.unwrap_or(&Type::Unknown(UnknownTypeKind::TypeError)), | ||
), | ||
|
||
// TODO annotated return type on `__new__` or metaclass `__call__` | ||
Type::Class(class) => CallOutcome::callable(Type::Instance(class)), | ||
|
||
// TODO: handle classes which implement the `__call__` protocol | ||
Type::Instance(_instance_ty) => CallOutcome::callable(Type::Unknown), | ||
Type::Instance(_instance_ty) => { | ||
CallOutcome::callable(Type::Unknown(UnknownTypeKind::RedKnotLimitation)) | ||
} | ||
|
||
// `Any` is callable, and its return type is also `Any`. | ||
Type::Any => CallOutcome::callable(Type::Any), | ||
|
||
Type::Unknown => CallOutcome::callable(Type::Unknown), | ||
Type::Unknown(kind) => CallOutcome::callable(Type::Unknown(kind)), | ||
|
||
Type::Union(union) => CallOutcome::union( | ||
self, | ||
|
@@ -555,7 +569,9 @@ impl<'db> Type<'db> { | |
), | ||
|
||
// TODO: intersection types | ||
Type::Intersection(_) => CallOutcome::callable(Type::Unknown), | ||
Type::Intersection(_) => { | ||
CallOutcome::callable(Type::Unknown(UnknownTypeKind::RedKnotLimitation)) | ||
} | ||
|
||
_ => CallOutcome::not_callable(self), | ||
} | ||
|
@@ -622,13 +638,13 @@ impl<'db> Type<'db> { | |
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> { | ||
match self { | ||
Type::Any => Type::Any, | ||
Type::Unknown => Type::Unknown, | ||
Type::Unbound => Type::Unknown, | ||
Type::Unknown(kind) => Type::Unknown(*kind), | ||
Type::Unbound => Type::Unknown(UnknownTypeKind::TypeError), | ||
Type::Never => Type::Never, | ||
Type::Class(class) => Type::Instance(*class), | ||
Type::Union(union) => union.map(db, |element| element.to_instance(db)), | ||
// TODO: we can probably do better here: --Alex | ||
Type::Intersection(_) => Type::Unknown, | ||
Type::Intersection(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation), | ||
// TODO: calling `.to_instance()` on any of these should result in a diagnostic, | ||
// since they already indicate that the object is an instance of some kind: | ||
Type::BooleanLiteral(_) | ||
|
@@ -641,7 +657,7 @@ impl<'db> Type<'db> { | |
| Type::StringLiteral(_) | ||
| Type::Tuple(_) | ||
| Type::LiteralString | ||
| Type::None => Type::Unknown, | ||
| Type::None => Type::Unknown(UnknownTypeKind::TypeError), | ||
} | ||
} | ||
|
||
|
@@ -667,9 +683,9 @@ impl<'db> Type<'db> { | |
// TODO: `type[Any]`? | ||
Type::Any => Type::Any, | ||
// TODO: `type[Unknown]`? | ||
Type::Unknown => Type::Unknown, | ||
Type::Unknown(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation), | ||
// TODO intersections | ||
Type::Intersection(_) => Type::Unknown, | ||
Type::Intersection(_) => Type::Unknown(UnknownTypeKind::RedKnotLimitation), | ||
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"), | ||
} | ||
} | ||
|
@@ -750,7 +766,9 @@ impl<'db> CallOutcome<'db> { | |
match (acc, ty) { | ||
(None, None) => None, | ||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)), | ||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))), | ||
(Some(builder), ty) => Some( | ||
builder.add(ty.unwrap_or(Type::Unknown(UnknownTypeKind::TypeError))), | ||
), | ||
} | ||
}) | ||
.map(UnionBuilder::build), | ||
|
@@ -786,7 +804,7 @@ impl<'db> CallOutcome<'db> { | |
not_callable_ty.display(db) | ||
), | ||
); | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::TypeError) | ||
} | ||
Self::Union { | ||
outcomes, | ||
|
@@ -799,7 +817,7 @@ impl<'db> CallOutcome<'db> { | |
let return_ty = match outcome { | ||
Self::NotCallable { not_callable_ty } => { | ||
not_callable.push(*not_callable_ty); | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::TypeError) | ||
} | ||
Self::RevealType { | ||
return_ty, | ||
|
@@ -867,12 +885,76 @@ impl<'db> IterationOutcome<'db> { | |
Self::Iterable { element_ty } => element_ty, | ||
Self::NotIterable { not_iterable_ty } => { | ||
inference_builder.not_iterable_diagnostic(iterable_node, not_iterable_ty); | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::TypeError) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// When it really matters to distinguish between different kinds of `Unknown` types | ||
// (see `PartialEq` implementation), we use the representation as `u8` | ||
#[derive(Debug, Clone, Copy, Eq)] | ||
#[repr(u8)] | ||
pub enum UnknownTypeKind { | ||
/// Temporary variant that indicates that we *should* | ||
/// be able to infer a type here in due course, but currently can't | ||
RedKnotLimitation, | ||
|
||
/// Invalid syntax in the node means we can't infer a type here | ||
InvalidSyntax, | ||
|
||
/// An `Unknown` type stemming from some kind of type error. | ||
/// | ||
/// Examples: | ||
/// - A function was called with argument(s) of incorrect type(s), leading | ||
/// to a diagnostic being emitted and the expression being evaluated as `Unknown`. | ||
/// - An expression was deemed to take place between two invalid types, e.g. `1 + "foo"`. | ||
/// (This is really the same as the first example, since expressions ultimately desguar | ||
/// to function calls, e.g. `1 + "foo"` desugars approximately to `type(1).__add__("foo")`.) | ||
/// - An attribute or method was looked up on an instance of a type that doesn't have that | ||
/// attribute or method. | ||
TypeError, | ||
|
||
/// The symbol was unannotated and the true type can't be reasonably inferred | ||
UnannotatedSymbol, | ||
Comment on lines
+917
to
+919
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that this case exists, and I think we should limit the cases to those we actually use, not add more speculatively. |
||
|
||
/// The type of symbols imported by imports that could not be resolved | ||
UnresolvedImport, | ||
|
||
/// A "second-order" Unknown type, that results from an interaction with | ||
/// a "first-order" Unknown type. For example, if the type of `x` is `Unknown`, | ||
/// the type of `x + 1` will also be `Unknown` | ||
SecondOrder, | ||
Comment on lines
+924
to
+927
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also not convinced we need this kind; I think I'd rather propagate the original unknown kind in the |
||
|
||
/// Special kind for intersections that is equal to all other kinds. This allows to consider a | ||
/// single `Type::Unknown(_)` in intersections. Should not be used in other contexts. | ||
AllKinds, | ||
Comment on lines
+929
to
+931
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also not convinced we should have this; I think I'd prefer to just not simplify differing unknowns in intersections. I don't expect this case to arise often, because most intersections will come from narrowing constraints, and in most cases we'll just not return a narrowing constraint at all rather than return one with an unknown type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear, does that mean that having potentially different unknown kinds inside an intersection is not a concern? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't think that's a problem. |
||
} | ||
|
||
impl PartialEq for UnknownTypeKind { | ||
fn eq(&self, other: &Self) -> bool { | ||
// The special kind `AllKinds` is equal to all other kinds | ||
match (self, other) { | ||
(UnknownTypeKind::AllKinds, _) | (_, UnknownTypeKind::AllKinds) => true, | ||
_ => *self as u8 == *other as u8, | ||
} | ||
} | ||
} | ||
|
||
impl std::hash::Hash for UnknownTypeKind { | ||
// Deriving `Hash` with an explicit implementation of `PartialEq` is flagged by clippy | ||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||
(*self as u8).hash(state); | ||
} | ||
} | ||
|
||
impl UnknownTypeKind { | ||
/// Method to aggregate multiple `UnknownTypeKind`s when they interact in any context | ||
pub(crate) fn union(self, _other: Self) -> Self { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the kind of an unknown should just always be a bitflag, and then the behavior of union is clear. (It doesn't make sense to me to have an enum with a special variant which is a bitflag of other variants; that seems like complexity that doesn't buy anything useful.) |
||
self | ||
} | ||
} | ||
|
||
#[salsa::interned] | ||
pub struct FunctionType<'db> { | ||
/// name of the function at definition | ||
|
@@ -914,13 +996,12 @@ impl<'db> FunctionType<'db> { | |
panic!("Function type definition must have `DefinitionKind::Function`") | ||
}; | ||
|
||
// TODO if a function `bar` is decorated by `foo`, | ||
// where `foo` is annotated as returning a type `X` that is a subtype of `Callable`, | ||
// we need to infer the return type from `X`'s return annotation | ||
// rather than from `bar`'s return annotation | ||
// in order to determine the type that `bar` returns | ||
// TODO if a function `bar` is decorated by `foo`, where `foo` is annotated as returning a | ||
// type `X` that is a subtype of `Callable`, we need to infer the return type from `X`'s | ||
// return annotation rather than from `bar`'s return annotation in order to determine the | ||
// type that `bar` returns | ||
if !function_stmt_node.decorator_list.is_empty() { | ||
return Type::Unknown; | ||
return Type::Unknown(UnknownTypeKind::RedKnotLimitation); | ||
} | ||
|
||
function_stmt_node | ||
|
@@ -929,12 +1010,13 @@ impl<'db> FunctionType<'db> { | |
.map(|returns| { | ||
if function_stmt_node.is_async { | ||
// TODO: generic `types.CoroutineType`! | ||
Type::Unknown | ||
Type::Unknown(UnknownTypeKind::RedKnotLimitation) | ||
} else { | ||
definition_expression_ty(db, definition, returns.as_ref()) | ||
} | ||
}) | ||
.unwrap_or(Type::Unknown) | ||
// TODO: can't we figure out the return type from the function body? | ||
.unwrap_or(Type::Unknown(UnknownTypeKind::RedKnotLimitation)) | ||
} | ||
} | ||
|
||
|
@@ -1075,7 +1157,9 @@ pub struct TupleType<'db> { | |
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::{builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType}; | ||
use super::{ | ||
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType, UnknownTypeKind, | ||
}; | ||
use crate::db::tests::TestDb; | ||
use crate::program::{Program, SearchPathSettings}; | ||
use crate::python_version::PythonVersion; | ||
|
@@ -1122,7 +1206,7 @@ mod tests { | |
fn into_type(self, db: &TestDb) -> Type<'_> { | ||
match self { | ||
Ty::Never => Type::Never, | ||
Ty::Unknown => Type::Unknown, | ||
Ty::Unknown => Type::Unknown(super::UnknownTypeKind::TypeError), | ||
Ty::Any => Type::Any, | ||
Ty::IntLiteral(n) => Type::IntLiteral(n), | ||
Ty::StringLiteral(s) => { | ||
|
@@ -1205,4 +1289,25 @@ mod tests { | |
|
||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db))); | ||
} | ||
|
||
#[test] | ||
fn unknown_of_different_kinds_are_equal_to_all_kinds() { | ||
let kind1 = UnknownTypeKind::TypeError; | ||
let kind2 = UnknownTypeKind::InvalidSyntax; | ||
let all_kinds = UnknownTypeKind::AllKinds; | ||
assert_ne!(kind1, kind2); | ||
assert_eq!(kind1, all_kinds); | ||
assert_eq!(kind2, all_kinds); | ||
} | ||
|
||
#[test] | ||
fn unknown_union_does_not_change_same_kinds() { | ||
let uk0 = UnknownTypeKind::TypeError; | ||
let uk1 = UnknownTypeKind::InvalidSyntax; | ||
assert_ne!(uk0, uk1); | ||
assert_eq!(uk0.union(uk0), uk0); | ||
assert_eq!(uk1.union(uk1), uk1); | ||
// TODO: unit test the outcome of != kind of unions once the correct implementation is | ||
// decided | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I like the name
NotImplemented
here