From 280062932f795504b46d8402e76d2f77db18c98a Mon Sep 17 00:00:00 2001 From: Amer Elsheikh Date: Sat, 3 Oct 2026 11:41:53 +0000 Subject: [PATCH] Suggest valid type forms when type(...) is called in annotations --- pyrefly/lib/alt/expr.rs | 2 +- pyrefly/lib/alt/solve.rs | 37 ++++++++++++++++++++++++++++++- pyrefly/lib/alt/type_level_dsl.rs | 2 +- pyrefly/lib/test/annotation.rs | 33 +++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 164368b86b..6a0cc80e33 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -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), ); } } diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 7b0b088c68..71260716c4 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -891,6 +891,39 @@ 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 { @@ -898,6 +931,8 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { "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") }; @@ -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); diff --git a/pyrefly/lib/alt/type_level_dsl.rs b/pyrefly/lib/alt/type_level_dsl.rs index 17fbd57001..80335303d5 100644 --- a/pyrefly/lib/alt/type_level_dsl.rs +++ b/pyrefly/lib/alt/type_level_dsl.rs @@ -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), ), } } diff --git a/pyrefly/lib/test/annotation.rs b/pyrefly/lib/test/annotation.rs index 4a7e39d69d..f3a1ae5090 100644 --- a/pyrefly/lib/test/annotation.rs +++ b/pyrefly/lib/test/annotation.rs @@ -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 +"#, +);