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
2 changes: 1 addition & 1 deletion pyrefly/lib/alt/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors,
x.range(),
ErrorKind::InvalidAnnotation,
"Function call cannot be used in annotations".to_owned(),
self.invalid_annotation_call_message(x),
);
}
}
Expand Down
37 changes: 36 additions & 1 deletion pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,13 +891,48 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
}
}

pub(crate) fn invalid_annotation_call_message(&self, call: &ExprCall) -> String {
let swallower = self.error_swallower();
let is_type = matches!(
self.expr_infer(&call.func, &swallower),
Type::ClassDef(cls) if cls.is_builtin("type")
);
let suggestion =
if is_type && call.arguments.args.len() == 1 && call.arguments.keywords.is_empty() {
match self.expr_infer(&call.arguments.args[0], &swallower) {
Type::ClassType(cls)
if cls.is_builtin("_NotImplementedType")
|| cls.has_qname("types", "NotImplementedType") =>
{
Some("`types.NotImplementedType`".to_owned())
}
Type::ClassType(cls) if &cls == self.stdlib.ellipsis_type() => {
Some("`types.EllipsisType`".to_owned())
}
Type::None => Some("`None`".to_owned()),
Type::Module(_) => Some("`types.ModuleType`".to_owned()),
Type::ClassDef(cls) => Some(format!("`type[{}]`", cls.name())),
_ => None,
}
} else {
None
};
if let Some(suggestion) = suggestion {
format!("Function call cannot be used in annotations. Did you mean {suggestion}?")
} else {
"Function call cannot be used in annotations".to_owned()
}
}

pub(crate) fn has_valid_annotation_syntax(&self, x: &Expr, errors: &ErrorCollector) -> bool {
if let Some(problem) = Ast::annotation_syntax_problem(x) {
let message = if let Expr::BinOp(ExprBinOp { op, .. }) = x {
format!(
"Binary operation `{}` cannot be used in annotations",
op.as_str()
)
} else if let Expr::Call(call) = x {
self.invalid_annotation_call_message(call)
} else {
format!("{problem} cannot be used in annotations")
};
Expand Down Expand Up @@ -4088,7 +4123,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
&& !is_pinned
&& self.may_be_implicit_type_alias(&ty)
&& !is_in_function_scope
&& self.has_valid_annotation_syntax(expr, &self.error_swallower())
&& Ast::annotation_syntax_problem(expr).is_none()
{
// Handle the possibility that we need to treat the type as a type alias
let ta = self.as_type_alias(name, TypeAliasStyle::LegacyImplicit, ty, expr, errors);
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/alt/type_level_dsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
errors,
call.range(),
ErrorKind::InvalidAnnotation,
"Function call cannot be used in annotations".to_owned(),
self.invalid_annotation_call_message(call),
),
}
}
Expand Down
33 changes: 33 additions & 0 deletions pyrefly/lib/test/annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,36 @@ def use(value: Base) -> None:
assert_type(value, make_type()) # E: Function call cannot be used in annotations
"#,
);

testcase!(
test_type_call_in_annotations,
TestEnv::new_with_version(PythonVersion::new(3, 13, 0)),
r#"
import types
from typing import TypeVar, Union, cast

class MyClass: ...
instance = MyClass()

def not_impl_param(x: type(NotImplemented)) -> None: ... # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?
def not_impl_union_param(x: Union[bool, type(NotImplemented)]) -> None: ... # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?
def not_impl_return() -> type(NotImplemented): ... # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?
def not_impl_union_return() -> Union[bool, type(NotImplemented)]: ... # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?
def not_impl_bitor_return() -> bool | type(NotImplemented): ... # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?
var_not_impl: type(NotImplemented) # E: Function call cannot be used in annotations. Did you mean `types.NotImplementedType`?

T = TypeVar("T", int, type(None)) # E: Function call cannot be used in annotations. Did you mean `None`?
var_none: type(None) # E: Function call cannot be used in annotations. Did you mean `None`?
var_ellipsis: type(Ellipsis) # E: Function call cannot be used in annotations. Did you mean `types.EllipsisType`?
var_dots: type(...) # E: Function call cannot be used in annotations. Did you mean `types.EllipsisType`?
var_module: type(types) # E: Function call cannot be used in annotations. Did you mean `types.ModuleType`?

var_cls: type(MyClass) # E: Function call cannot be used in annotations. Did you mean `type[MyClass]`?
def cls_return() -> list[type(MyClass)]: ... # E: Function call cannot be used in annotations. Did you mean `type[MyClass]`?
def cls_cast(x: object) -> None:
cast(type(MyClass), x) # E: Function call cannot be used in annotations. Did you mean `type[MyClass]`?

var_instance: type(instance) # E: Function call cannot be used in annotations
self_ref: type(self_ref) = 1 # E: Function call cannot be used in annotations
"#,
);
Loading