Compare commits

...

2 Commits

Author SHA1 Message Date
Alex Waygood
446a1ba2e1 example implementation of is_disjoint_from 2025-08-26 09:57:16 +01:00
Alex Waygood
1c7f3cd2b8 Prototype code structure for NewType implementation 2025-08-25 23:13:36 +01:00
6 changed files with 203 additions and 0 deletions

View File

@@ -60,6 +60,7 @@ pub use crate::types::ide_support::{
use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::newtype::{NewTypeInstance, NewTypePseudoClass};
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature};
use crate::types::tuple::TupleSpec;
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
@@ -89,6 +90,7 @@ mod infer;
mod instance;
mod mro;
mod narrow;
mod newtype;
mod protocol_class;
mod signatures;
mod special_form;
@@ -617,6 +619,18 @@ pub enum Type<'db> {
/// The set of Python objects with the given class in their __class__'s method resolution order.
/// Construct this variant using the `Type::instance` constructor function.
NominalInstance(NominalInstanceType<'db>),
/// The set of Python objects that are considered by ty to inhabit a type created by a
/// call to `NewType(...)`.
///
/// For example:
///
/// ```py
/// from typing import NewType
/// UserId = NewType('UserId', int) # The call to `NewType` creates a pseudo-class `UserId`
/// x = UserId(42) # `x` inhabits the type `UserId`,
/// # which is represented in our model with `Type::NewTypeInstance`
/// ```
NewTypeInstance(NewTypeInstance<'db>),
/// The set of Python objects that conform to the interface described by a given protocol.
/// Construct this variant using the `Type::instance` constructor function.
ProtocolInstance(ProtocolInstanceType<'db>),
@@ -2273,6 +2287,15 @@ impl<'db> Type<'db> {
C::from_bool(db, !known_instance.is_instance_of(db, instance.class(db)))
}
(Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => {
left.is_disjoint_from(db, right)
}
(Type::NewTypeInstance(new_type_instance), other)
| (other, Type::NewTypeInstance(new_type_instance)) => new_type_instance
.supertype(db)
.is_disjoint_from_impl(db, other, visitor),
(Type::BooleanLiteral(..) | Type::TypeIs(_), Type::NominalInstance(instance))
| (Type::NominalInstance(instance), Type::BooleanLiteral(..) | Type::TypeIs(_)) => {
// A `Type::BooleanLiteral()` must be an instance of exactly `bool`
@@ -4963,6 +4986,7 @@ impl<'db> Type<'db> {
}
let special_case = match self {
Type::NewTypeInstance(newtype_instance) => newtype_instance.tuple_spec(db),
Type::NominalInstance(nominal) => nominal.tuple_spec(db),
Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => {
Some(Cow::Owned(TupleSpec::homogeneous(todo_type!(
@@ -5598,6 +5622,9 @@ impl<'db> Type<'db> {
.map(Type::NonInferableTypeVar)
.unwrap_or(*self))
}
KnownInstanceType::NewTypePseudoClass(newtype) => {
Ok(Type::NewTypeInstance(newtype.to_instance()))
}
KnownInstanceType::Deprecated(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Deprecated],
fallback_type: Type::unknown(),
@@ -6686,6 +6713,9 @@ pub enum KnownInstanceType<'db> {
/// A single instance of `dataclasses.Field`
Field(FieldInstance<'db>),
/// The object returned by a call to `typing.NewType`.
NewTypePseudoClass(NewTypePseudoClass<'db>),
}
fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -6741,6 +6771,7 @@ impl<'db> KnownInstanceType<'db> {
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::Deprecated(_) => KnownClass::Deprecated,
Self::Field(_) => KnownClass::Field,
Self::NewTypePseudoClass(_) => KnownClass::NewType,
}
}
@@ -6791,6 +6822,13 @@ impl<'db> KnownInstanceType<'db> {
field.default_type(self.db).display(self.db).fmt(f)?;
f.write_str("]")
}
KnownInstanceType::NewTypePseudoClass(new_type) => {
write!(
f,
"<Pseudo-class object for NewType `{}`>",
new_type.name(self.db)
)
}
}
}
}

View File

@@ -27,6 +27,7 @@ use crate::types::function::{
DataclassTransformerParams, FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral,
};
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
use crate::types::newtype::NewTypePseudoClass;
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::tuple::{Tuple, TupleLength, TupleType};
use crate::types::{
@@ -1055,6 +1056,36 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownClass::NewType) => match overload.parameter_types() {
[Some(Type::StringLiteral(name)), Some(supertype)] => {
let pseudo_class =
if let Some(supertype) = supertype.to_class_type(db) {
NewTypePseudoClass::from_class(
db,
&name.value(db),
supertype,
definition,
)
} else if let Type::KnownInstance(
KnownInstanceType::NewTypePseudoClass(newtype_class),
) = supertype
{
NewTypePseudoClass::from_new_type(
db,
&name.value(db),
*newtype_class,
definition,
)
} else {
continue;
};
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::NewTypePseudoClass(pseudo_class),
));
}
_ => {}
},
_ => {}
},

View File

@@ -164,6 +164,7 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::TypeVar(_)
| KnownInstanceType::Deprecated(_)
| KnownInstanceType::NewTypePseudoClass(_)
| KnownInstanceType::Field(_) => None,
},

View File

@@ -124,6 +124,9 @@ impl Display for DisplayRepresentation<'_> {
(ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings).fmt(f),
}
}
Type::NewTypeInstance(newtype_instance) => {
f.write_str(newtype_instance.name(self.db))
}
Type::ProtocolInstance(protocol) => match protocol.inner {
Protocol::FromClass(ClassType::NonGeneric(class)) => {
f.write_str(class.name(self.db))

View File

@@ -10390,6 +10390,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(&subscript.slice);
todo_type!("Generic PEP-695 type alias")
}
KnownInstanceType::NewTypePseudoClass(newtype) => {
self.infer_type_expression(&subscript.slice);
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"NewType `{}` expected no type arguments",
newtype.name(self.db())
));
}
Type::unknown()
}
},
Type::Dynamic(DynamicType::Todo(_)) => {
self.infer_type_expression(slice);

View File

@@ -0,0 +1,120 @@
use std::borrow::Cow;
use ruff_python_ast::name::Name;
use crate::{
Db,
semantic_index::definition::Definition,
types::{ClassType, Type, constraints::Constraints, tuple::TupleSpec},
};
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update, PartialOrd, Ord,
)]
pub struct NewTypePseudoClass<'db>(NewType<'db>);
impl<'db> NewTypePseudoClass<'db> {
pub(crate) fn from_class(
db: &'db dyn Db,
name: &str,
supertype: ClassType<'db>,
definition: Definition<'db>,
) -> Self {
Self(NewType::new(
db,
Name::from(name),
NewTypeBase::Class(supertype),
definition,
))
}
pub(crate) fn from_new_type(
db: &'db dyn Db,
name: &str,
supertype: NewTypePseudoClass<'db>,
definition: Definition<'db>,
) -> Self {
Self(NewType::new(
db,
Name::from(name),
NewTypeBase::NewType(supertype.0),
definition,
))
}
pub(crate) fn to_instance(&self) -> NewTypeInstance<'db> {
NewTypeInstance(self.0)
}
pub(crate) fn name(&self, db: &'db dyn Db) -> &'db str {
self.0.name(db)
}
}
#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update, PartialOrd, Ord,
)]
pub struct NewTypeInstance<'db>(NewType<'db>);
impl<'db> NewTypeInstance<'db> {
pub(crate) fn tuple_spec(self, db: &'db dyn Db) -> Option<Cow<TupleSpec<'db>>> {
match self.0.supertype(db) {
NewTypeBase::Class(class) => Type::instance(db, class).tuple_instance_spec(db),
NewTypeBase::NewType(newtype) => NewTypeInstance(newtype).tuple_spec(db),
}
}
pub(crate) fn supertype(self, db: &'db dyn Db) -> Type<'db> {
match self.0.supertype(db) {
NewTypeBase::Class(class) => Type::instance(db, class),
NewTypeBase::NewType(newtype) => Type::NewTypeInstance(NewTypeInstance(newtype)),
}
}
pub(crate) fn has_relation_to<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: NewTypeInstance<'db>,
) -> C {
if self == other {
return C::from_bool(db, true);
}
match self.0.supertype(db) {
NewTypeBase::Class(_) => C::from_bool(db, false),
NewTypeBase::NewType(newtype) => NewTypeInstance(newtype).has_relation_to(db, other),
}
}
pub(crate) fn is_disjoint_from<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: NewTypeInstance<'db>,
) -> C {
C::from_bool(
db,
!(self.has_relation_to(db, other) || other.has_relation_to(db, self)),
)
}
pub(crate) fn name(&self, db: &'db dyn Db) -> &'db str {
self.0.name(db)
}
}
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
struct NewType<'db> {
#[returns(ref)]
name: Name,
supertype: NewTypeBase<'db>,
definition: Definition<'db>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for NewType<'_> {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
enum NewTypeBase<'db> {
Class(ClassType<'db>),
NewType(NewType<'db>),
}