Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyrefly/lib/alt/class/class_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,15 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
Some(ty) => parse_base_class_type(ty),
}
}
BaseClass::Call(x) => {
// Ignore all type errors here since they'll be reported in `class_bases_of` anyway.
let errors = ErrorCollector::new(self.module().dupe(), ErrorStyle::Never);
let ty = self.expr_infer(x, &errors);
match self.untype_opt(ty.clone(), x.range(), &errors) {
None => BaseClassParseResult::InvalidType(ty, x.range()),
Some(ty) => parse_base_class_type(ty),
}
}
BaseClass::NamedTuple(..) => parse_base_class_type(
self.heap
.mk_class_type(self.stdlib.named_tuple_fallback().clone()),
Expand Down
35 changes: 20 additions & 15 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,20 +496,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
}
}

/// Solve a `BindingLegacyTypeParam`, producing a `LegacyTypeParameterLookup` that tells the
/// caller whether the name is a type parameter and, if so, which `Quantified` to use.
///
/// The `scope_anchor` is the range of the `KeyLegacyTypeParam` key itself — the first
/// occurrence of the TypeVar name in this scope. It is unique per (scope, TypeVar) pair:
/// two functions that both use an imported `T` will have different first-occurrence ranges,
/// so their `Quantified`s will have different identities even though they share the same
/// module-level `TypeVar` declaration.
pub fn solve_legacy_tparam(
&self,
binding: &BindingLegacyTypeParam,
scope_anchor: TextRange,
) -> LegacyTypeParameterLookup {
let maybe_parameter = match binding {
/// Resolve the original value before a possible legacy type parameter is quantified.
pub(crate) fn legacy_tparam_value(&self, binding: &BindingLegacyTypeParam) -> TypeInfo {
match binding {
BindingLegacyTypeParam::ParamKeyed(k) => self.get_idx(*k).clone(),
BindingLegacyTypeParam::ModuleKeyed(module) => {
// Errors in attribute lookup are reported elsewhere.
Expand All @@ -526,7 +515,23 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
)
})
}
};
}
}

/// Solve a `BindingLegacyTypeParam`, producing a `LegacyTypeParameterLookup` that tells the
/// caller whether the name is a type parameter and, if so, which `Quantified` to use.
///
/// The `scope_anchor` is the range of the `KeyLegacyTypeParam` key itself — the first
/// occurrence of the TypeVar name in this scope. It is unique per (scope, TypeVar) pair:
/// two functions that both use an imported `T` will have different first-occurrence ranges,
/// so their `Quantified`s will have different identities even though they share the same
/// module-level `TypeVar` declaration.
pub fn solve_legacy_tparam(
&self,
binding: &BindingLegacyTypeParam,
scope_anchor: TextRange,
) -> LegacyTypeParameterLookup {
let maybe_parameter = self.legacy_tparam_value(binding);
// Use the scope_anchor (the KeyLegacyTypeParam's own range, i.e. the first occurrence
// of this TypeVar name in the enclosing function/class/alias scope) as the identity
// anchor. This gives each (scope, TypeVar) pair a distinct Quantified even when multiple
Expand Down
33 changes: 33 additions & 0 deletions pyrefly/lib/alt/types/class_bases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ use pyrefly_util::display::commas_iter;
use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
use starlark_map::Hashed;
use starlark_map::small_map::SmallMap;

use crate::alt::answers::LookupAnswer;
use crate::alt::answers_solver::AnswersSolver;
use crate::alt::solve::TypeFormContext;
use crate::binding::base_class::BaseClass;
use crate::binding::base_class::BaseClassExpr;
use crate::binding::binding::Key;
use crate::binding::binding::KeyTParams;
use crate::config::error_kind::ErrorKind;
use crate::error::collector::ErrorCollector;
use crate::error::style::ErrorStyle;
Expand Down Expand Up @@ -347,6 +350,36 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
}
Some((ty, x.range()))
}
BaseClass::Call(x) => {
let mut ty =
self.expr_untype(x, TypeFormContext::BaseClassList, &fake_error_collector);
// Call arguments retain raw TypeVars, including parameters bound by this class.
if let Some(idx) = self
.bindings()
.key_to_idx_hashed_opt(Hashed::new(&KeyTParams(cls.index())))
{
let quantified = self
.bindings()
.get(idx)
.legacy_tparams
.iter()
.filter_map(|key| {
let parameter = self.get_idx(*key).parameter()?;
let value = self.legacy_tparam_value(self.bindings().get(*key));
Some((value.into_ty(), parameter))
})
.collect::<SmallMap<_, _>>();
ty.transform_types_in_type_variable_positions(&mut |t| {
if let Some(parameter) = quantified.get(t) {
*t = (*parameter).clone().to_type(self.heap);
}
});
}
if !is_new_type {
self.check_legacy_typevar_scoping(&mut ty, x.range(), errors);
}
Some((ty, x.range()))
}
BaseClass::NamedTuple(..) => Some((
self.heap
.mk_class_type(self.stdlib.named_tuple_fallback().clone()),
Expand Down
3 changes: 3 additions & 0 deletions pyrefly/lib/binding/base_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ pub enum BaseClass {
TypedDict(TextRange),
Generic(BaseClassGeneric),
BaseClassExpr(BaseClassExpr),
/// A call expression used as a base of an ordinary class statement.
Call(Expr),
InvalidExpr(Expr),
/// A namedtuple base class.
/// The bool indicates whether fields were dynamically generated
Expand Down Expand Up @@ -131,6 +133,7 @@ impl Ranged for BaseClass {
BaseClass::TypedDict(range) => *range,
BaseClass::Generic(x) => x.range(),
BaseClass::BaseClassExpr(base_expr) => base_expr.range(),
BaseClass::Call(expr) => expr.range(),
BaseClass::InvalidExpr(expr) => expr.range(),
BaseClass::NamedTuple(range, _) => *range,
BaseClass::SynthesizedBase(_, range) => *range,
Expand Down
17 changes: 15 additions & 2 deletions pyrefly/lib/binding/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,20 @@ impl<'a> BindingsBuilder<'a> {
},
);

let base_class = self.base_class_of(base.clone());
// Call bases require the delayed validation installed for ordinary class statements.
let base_class = match self.base_class_of(base.clone()) {
BaseClass::InvalidExpr(Expr::Call(call))
if !matches!(
self.as_special_export(&call.func),
Some(
SpecialExport::CollectionsNamedTuple | SpecialExport::TypingNamedTuple
)
) =>
{
BaseClass::Call(Expr::Call(call))
}
base_class => base_class,
};
// NOTE(grievejia): If any of the class base is a specialized generic class (e.g. `Foo[Bar]`), and if the tparam of the
// generic class has a bound or constraint, we won't be validating the type `Bar` against that bound/constraint of the
// tparam eagerly in order to avoid dependency cycle. But the validation needs to happen somewhere.
Expand All @@ -373,7 +386,7 @@ impl<'a> BindingsBuilder<'a> {
// usage tracking.
if matches!(
base_class,
BaseClass::BaseClassExpr(..) | BaseClass::TypeOf(..)
BaseClass::BaseClassExpr(..) | BaseClass::Call(..) | BaseClass::TypeOf(..)
) {
self.insert_binding(
KeyExpect::TypeCheckBaseClassExpr(base.range()),
Expand Down
103 changes: 103 additions & 0 deletions pyrefly/lib/test/class_subtyping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,109 @@ assert_type(B.x, int)
"#,
);

testcase!(
test_call_returning_type_in_base_class_list,
r#"
from typing import Any, assert_type

class Mixin:
def greet(self) -> str:
return "hi"

def factory(name: str) -> type[Mixin]: ...
def untyped_factory(name: str) -> Any: ...

class C(factory("x")):
pass

class D(untyped_factory("x")):
pass

assert_type(C().greet(), str)
"#,
);

testcase!(
test_call_base_legacy_typevar_scoping,
r#"
from typing import Any, TypeVar, assert_type

T = TypeVar("T")
S = TypeVar("S")

def ident(x: type[S]) -> type[S]: ...

class C(ident(list[T])): # E: Type variable `T` is not in scope
pass

assert_type(C()[0], Any)

class D(ident(list[int])):
pass

assert_type(D()[0], int)
"#,
);

testcase!(
test_call_base_bound_legacy_typevar,
r#"
from typing import Generic, TypeVar, assert_type

T = TypeVar("T")
S = TypeVar("S")

def ident(x: type[S]) -> type[S]: ...

class C(ident(list[T]), Generic[T]):
pass

assert_type(C[int]()[0], int)
assert_type(C[str]()[0], str)
"#,
);

testcase!(
test_call_base_imported_legacy_typevars,
TestEnv::one(
"params",
r#"
from typing import TypeVar
T = TypeVar("T")
"#,
),
r#"
from typing import Any, Generic, TypeVar, assert_type
import params

T = TypeVar("T")
S = TypeVar("S")

def ident(x: type[S]) -> type[S]: ...

class C(ident(list[params.T]), Generic[params.T]):
pass

assert_type(C[int]()[0], int)

class D(ident(list[T]), Generic[params.T]): # E: Type variable `T` is not in scope
pass

assert_type(D[int]()[0], Any)
"#,
);

testcase!(
test_call_base_argument_validation,
r#"
def factory(name: str) -> type[int]:
return int

class C(factory(1)): # E: Argument `Literal[1]` is not assignable to parameter `name` with type `str`
pass
"#,
);

testcase!(
test_type_function_in_base_class_list_self_cycle,
r#"
Expand Down
18 changes: 18 additions & 0 deletions pyrefly/lib/test/new_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ userId = NewType() # E: Missing argument `name` # E: Missing argument `tp`
"#,
);

testcase!(
test_new_type_call_base,
r#"
from typing import NewType

def factory(name: str) -> type[int]:
name.upper()
return int

def returns_none() -> None:
return None

ValidCall = NewType("ValidCall", factory("x")) # E: Second argument to NewType is invalid
InvalidCall = NewType("InvalidCall", factory(1)) # E: Second argument to NewType is invalid
NoneCall = NewType("NoneCall", returns_none()) # E: Second argument to NewType is invalid
"#,
);

testcase!(
test_new_type_not_allowed,
r#"
Expand Down
Loading