Skip to content

Commit

Permalink
[red-knot] rename {Class,Module,Function} => {Class,Module,Function}L…
Browse files Browse the repository at this point in the history
…iteral (#13873)

## Summary

* Rename `Type::Class` => `Type::ClassLiteral`
* Rename `Type::Function` => `Type::FunctionLiteral`
* Do not rename `Type::Module`
* Remove `*Literal` suffixes in `display::LiteralTypeKind` variants, as
per clippy suggestion
* Get rid of `Type::is_class()` in favor of `is_subtype_of(…, 'type')`;
modifiy `is_subtype_of` to support this.
* Add new `Type::is_xyz()` methods and use them instead of matching on
`Type` variants.

closes #13863 

## Test Plan

New `is_subtype_of_class_literals` unit test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
  • Loading branch information
sharkdp and AlexWaygood authored Oct 22, 2024
1 parent c6ce52c commit f335fe4
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 117 deletions.
7 changes: 3 additions & 4 deletions crates/red_knot_python_semantic/src/semantic_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ mod tests {
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::Type;
use crate::{HasTy, ProgramSettings, SemanticModel};

fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
Expand Down Expand Up @@ -205,7 +204,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = function.ty(&model);

assert!(matches!(ty, Type::Function(_)));
assert!(ty.is_function_literal());

Ok(())
}
Expand All @@ -222,7 +221,7 @@ mod tests {
let model = SemanticModel::new(&db, foo);
let ty = class.ty(&model);

assert!(matches!(ty, Type::Class(_)));
assert!(ty.is_class_literal());

Ok(())
}
Expand All @@ -243,7 +242,7 @@ mod tests {
let model = SemanticModel::new(&db, bar);
let ty = alias.ty(&model);

assert!(matches!(ty, Type::Class(_)));
assert!(ty.is_class_literal());

Ok(())
}
Expand Down
167 changes: 104 additions & 63 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,11 @@ pub enum Type<'db> {
/// `Todo` would change the output type.
Todo,
/// A specific function object
Function(FunctionType<'db>),
FunctionLiteral(FunctionType<'db>),
/// A specific module object
Module(File),
ModuleLiteral(File),
/// A specific class object
Class(ClassType<'db>),
ClassLiteral(ClassType<'db>),
/// The set of Python objects with the given class in their __class__'s method resolution order
Instance(ClassType<'db>),
/// The set of objects in any of the types in the union
Expand Down Expand Up @@ -292,28 +292,32 @@ impl<'db> Type<'db> {
matches!(self, Type::Never)
}

pub const fn into_class_type(self) -> Option<ClassType<'db>> {
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
match self {
Type::Class(class_type) => Some(class_type),
Type::ClassLiteral(class_type) => Some(class_type),
_ => None,
}
}

pub fn expect_class(self) -> ClassType<'db> {
self.into_class_type()
.expect("Expected a Type::Class variant")
pub fn expect_class_literal(self) -> ClassType<'db> {
self.into_class_literal_type()
.expect("Expected a Type::ClassLiteral variant")
}

pub const fn into_module_type(self) -> Option<File> {
pub const fn is_class_literal(&self) -> bool {
matches!(self, Type::ClassLiteral(..))
}

pub const fn into_module_literal_type(self) -> Option<File> {
match self {
Type::Module(file) => Some(file),
Type::ModuleLiteral(file) => Some(file),
_ => None,
}
}

pub fn expect_module(self) -> File {
self.into_module_type()
.expect("Expected a Type::Module variant")
pub fn expect_module_literal(self) -> File {
self.into_module_literal_type()
.expect("Expected a Type::ModuleLiteral variant")
}

pub const fn into_union_type(self) -> Option<UnionType<'db>> {
Expand All @@ -328,6 +332,10 @@ impl<'db> Type<'db> {
.expect("Expected a Type::Union variant")
}

pub const fn is_union(&self) -> bool {
matches!(self, Type::Union(..))
}

pub const fn into_intersection_type(self) -> Option<IntersectionType<'db>> {
match self {
Type::Intersection(intersection_type) => Some(intersection_type),
Expand All @@ -340,16 +348,20 @@ impl<'db> Type<'db> {
.expect("Expected a Type::Intersection variant")
}

pub const fn into_function_type(self) -> Option<FunctionType<'db>> {
pub const fn into_function_literal_type(self) -> Option<FunctionType<'db>> {
match self {
Type::Function(function_type) => Some(function_type),
Type::FunctionLiteral(function_type) => Some(function_type),
_ => None,
}
}

pub fn expect_function(self) -> FunctionType<'db> {
self.into_function_type()
.expect("Expected a Type::Function variant")
pub fn expect_function_literal(self) -> FunctionType<'db> {
self.into_function_literal_type()
.expect("Expected a Type::FunctionLiteral variant")
}

pub const fn is_function_literal(&self) -> bool {
matches!(self, Type::FunctionLiteral(..))
}

pub const fn into_int_literal_type(self) -> Option<i64> {
Expand All @@ -364,6 +376,14 @@ impl<'db> Type<'db> {
.expect("Expected a Type::IntLiteral variant")
}

pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}

pub const fn is_literal_string(&self) -> bool {
matches!(self, Type::LiteralString)
}

pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
match self {
Type::Unbound => true,
Expand All @@ -387,18 +407,8 @@ impl<'db> Type<'db> {

pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
match self {
Type::Class(class) => class.is_stdlib_symbol(db, module_name, name),
Type::Function(function) => function.is_stdlib_symbol(db, module_name, name),
_ => false,
}
}

/// Return true if the type is a class or a union of classes.
pub fn is_class(&self, db: &'db dyn Db) -> bool {
match self {
Type::Union(union) => union.elements(db).iter().all(|ty| ty.is_class(db)),
Type::Class(_) => true,
// / TODO include type[X], once we add that type
Type::ClassLiteral(class) => class.is_stdlib_symbol(db, module_name, name),
Type::FunctionLiteral(function) => function.is_stdlib_symbol(db, module_name, name),
_ => false,
}
}
Expand Down Expand Up @@ -436,6 +446,11 @@ impl<'db> Type<'db> {
{
true
}
(Type::ClassLiteral(..), Type::Instance(class))
if class.is_known(db, KnownClass::Type) =>
{
true
}
(Type::Union(union), ty) => union
.elements(db)
.iter()
Expand Down Expand Up @@ -517,17 +532,17 @@ impl<'db> Type<'db> {
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::Function(..)
| Type::Module(..)
| Type::Class(..)),
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)),
right @ (Type::None
| Type::BooleanLiteral(..)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
| Type::Function(..)
| Type::Module(..)
| Type::Class(..)),
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)),
) => left != right,

(Type::None, Type::Instance(class_type)) | (Type::Instance(class_type), Type::None) => {
Expand Down Expand Up @@ -577,12 +592,12 @@ impl<'db> Type<'db> {
(Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true,

(
Type::Function(..) | Type::Module(..) | Type::Class(..),
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
Type::Instance(class_type),
)
| (
Type::Instance(class_type),
Type::Function(..) | Type::Module(..) | Type::Class(..),
Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..),
) => !class_type.is_known(db, KnownClass::Object),

(Type::Instance(..), Type::Instance(..)) => {
Expand Down Expand Up @@ -643,7 +658,7 @@ impl<'db> Type<'db> {
// are both of type Literal[345], for example.
false
}
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
Type::None | Type::BooleanLiteral(_) | Type::FunctionLiteral(..) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) => true,
Type::Tuple(..) => {
// The empty tuple is a singleton on CPython and PyPy, but not on other Python
// implementations such as GraalPy. Its *use* as a singleton is discouraged and
Expand Down Expand Up @@ -675,9 +690,9 @@ impl<'db> Type<'db> {
pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool {
match self {
Type::None
| Type::Function(..)
| Type::Module(..)
| Type::Class(..)
| Type::FunctionLiteral(..)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
| Type::IntLiteral(..)
| Type::BooleanLiteral(..)
| Type::StringLiteral(..)
Expand Down Expand Up @@ -747,12 +762,12 @@ impl<'db> Type<'db> {
// TODO: attribute lookup on None type
Type::Todo
}
Type::Function(_) => {
Type::FunctionLiteral(_) => {
// TODO: attribute lookup on function type
Type::Todo
}
Type::Module(file) => global_symbol_ty(db, *file, name),
Type::Class(class) => class.class_member(db, name),
Type::ModuleLiteral(file) => global_symbol_ty(db, *file, name),
Type::ClassLiteral(class) => class.class_member(db, name),
Type::Instance(_) => {
// TODO MRO? get_own_instance_member, get_instance_member
Type::Todo
Expand Down Expand Up @@ -800,9 +815,9 @@ impl<'db> Type<'db> {
Truthiness::Ambiguous
}
Type::None => Truthiness::AlwaysFalse,
Type::Function(_) => Truthiness::AlwaysTrue,
Type::Module(_) => Truthiness::AlwaysTrue,
Type::Class(_) => {
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => {
// TODO: lookup `__bool__` and `__len__` methods on the class's metaclass
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
Truthiness::Ambiguous
Expand Down Expand Up @@ -847,7 +862,7 @@ impl<'db> Type<'db> {
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
match self {
// TODO validate typed call arguments vs callable signature
Type::Function(function_type) => match function_type.known(db) {
Type::FunctionLiteral(function_type) => match function_type.known(db) {
None => CallOutcome::callable(function_type.return_type(db)),
Some(KnownFunction::RevealType) => CallOutcome::revealed(
function_type.return_type(db),
Expand All @@ -856,7 +871,7 @@ impl<'db> Type<'db> {
},

// TODO annotated return type on `__new__` or metaclass `__call__`
Type::Class(class) => {
Type::ClassLiteral(class) => {
CallOutcome::callable(match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
Expand Down Expand Up @@ -976,17 +991,17 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
Type::Unbound => Type::Unknown,
Type::Never => Type::Never,
Type::Class(class) => Type::Instance(*class),
Type::ClassLiteral(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::Todo,
// 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(_)
| Type::BytesLiteral(_)
| Type::Function(_)
| Type::FunctionLiteral(_)
| Type::Instance(_)
| Type::Module(_)
| Type::ModuleLiteral(_)
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::Tuple(_)
Expand All @@ -1002,17 +1017,17 @@ impl<'db> Type<'db> {
match self {
Type::Unbound => Type::Unbound,
Type::Never => Type::Never,
Type::Instance(class) => Type::Class(*class),
Type::Instance(class) => Type::ClassLiteral(*class),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db),
Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db),
Type::IntLiteral(_) => KnownClass::Int.to_class(db),
Type::Function(_) => KnownClass::FunctionType.to_class(db),
Type::Module(_) => KnownClass::ModuleType.to_class(db),
Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class(db),
Type::Tuple(_) => KnownClass::Tuple.to_class(db),
Type::None => KnownClass::NoneType.to_class(db),
// TODO not accurate if there's a custom metaclass...
Type::Class(_) => KnownClass::Type.to_class(db),
Type::ClassLiteral(_) => KnownClass::Type.to_class(db),
// TODO can we do better here? `type[LiteralString]`?
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class(db),
// TODO: `type[Any]`?
Expand Down Expand Up @@ -1642,7 +1657,7 @@ impl<'db> ClassType<'db> {
// TODO: we need to iterate over the *MRO* here, not the bases
(other == self)
|| self.bases(db).any(|base| match base {
Type::Class(base_class) => base_class == other,
Type::ClassLiteral(base_class) => base_class == other,
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
// participate, so we should not return `True` if we find `Any/Unknown` in the
// bases.
Expand Down Expand Up @@ -1923,6 +1938,32 @@ mod tests {
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
}

#[test]
fn is_subtype_of_class_literals() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
U = A if flag else B
",
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();

let type_a = super::global_symbol_ty(&db, module, "A");
let type_u = super::global_symbol_ty(&db, module, "U");

assert!(type_a.is_class_literal());
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));

assert!(type_u.is_union());
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
assert!(type_u.is_subtype_of(&db, Ty::BuiltinInstance("object").into_type(&db)));
}

#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
Expand Down Expand Up @@ -2005,19 +2046,19 @@ mod tests {
"
class A: ...
class B: ...
x = A if flag else B
U = A if flag else B
",
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();

let type_a = super::global_symbol_ty(&db, module, "A");
let type_x = super::global_symbol_ty(&db, module, "x");
let type_u = super::global_symbol_ty(&db, module, "U");

assert!(matches!(type_a, Type::Class(_)));
assert!(matches!(type_x, Type::Union(_)));
assert!(type_a.is_class_literal());
assert!(type_u.is_union());

assert!(!type_a.is_disjoint_from(&db, type_x));
assert!(!type_a.is_disjoint_from(&db, type_u));
}

#[test_case(Ty::None)]
Expand Down
Loading

0 comments on commit f335fe4

Please sign in to comment.