diff --git a/pyrefly/lib/alt/function.rs b/pyrefly/lib/alt/function.rs index e606654fb0..b40655dce1 100644 --- a/pyrefly/lib/alt/function.rs +++ b/pyrefly/lib/alt/function.rs @@ -736,7 +736,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { .forall(tparams); ty = self.move_return_tparams_of_type(ty); for (x, range) in def.decorators.iter().rev() { - ty = self.apply_function_decorator(x.clone(), ty, &def.metadata, *range, errors); + ty = self.apply_function_decorator( + x.clone(), + ty, + &def.metadata, + &stmt.name, + *range, + errors, + ); } Arc::new(ty) } @@ -1596,11 +1603,59 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } + fn decorator_missing_injected_parameter_message( + &self, + decorator: &Type, + decoratee_name: &Identifier, + decoratee: &Type, + ) -> Option { + let decorator_name = decorator + .visit_toplevel_func_metadata(&|meta| Some(meta.kind.function_name().to_string()))?; + let expected_decoratee = decorator.callable_first_param(self.heap)?; + let expected_signature = expected_decoratee + .callable_signatures() + .into_iter() + .find(|signature| { + matches!(&signature.params, Params::ParamSpec(prefix, _) if !prefix.is_empty()) + })?; + + let actual_signature = match decoratee { + Type::Function(func) => &func.signature, + Type::Callable(callable) => callable.as_ref(), + _ => return None, + }; + + let Params::ParamSpec(prefix, _) = &expected_signature.params else { + return None; + }; + let Params::List(actual_params) = &actual_signature.params else { + return None; + }; + if actual_params.len() + 1 != prefix.len() { + return None; + } + + let missing = prefix.get(actual_params.len())?; + let missing_ty = match missing { + PrefixParam::PosOnly(_, ty, Required::Required) + | PrefixParam::Pos(_, ty, Required::Required) => ty, + PrefixParam::PosOnly(_, _, Required::Optional(_)) + | PrefixParam::Pos(_, _, Required::Optional(_)) => return None, + }; + Some(format!( + "Function `{}` is missing parameter of type `{}` injected by decorator `{}`", + decoratee_name.as_str(), + self.for_display(missing_ty.clone()), + decorator_name, + )) + } + fn apply_function_decorator( &self, decorator: Type, decoratee: Type, metadata: &FuncMetadata, + decoratee_name: &Identifier, range: TextRange, errors: &ErrorCollector, ) -> Type { @@ -1610,6 +1665,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { { return decoratee; } + let decorator_for_message = decorator.clone(); let application = self.prepare_decorator_application(decorator, decoratee, range, errors); // Run a decorator call, buffering errors so we can decide between the primary // and Self-rewritten fallback without double-reporting. @@ -1638,6 +1694,17 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { && fallback_errors.is_empty() { fallback_return + } else if let Some(message) = self.decorator_missing_injected_parameter_message( + &decorator_for_message, + decoratee_name, + &application.decoratee_arg, + ) { + self.error( + errors, + decoratee_name.range(), + ErrorKind::InvalidDecorator, + message, + ) } else { errors.extend(primary_errors); primary_return diff --git a/pyrefly/lib/test/decorators.rs b/pyrefly/lib/test/decorators.rs index e990cfd830..9933a79ac9 100644 --- a/pyrefly/lib/test/decorators.rs +++ b/pyrefly/lib/test/decorators.rs @@ -419,6 +419,23 @@ g(f) "#, ); +testcase!( + test_decorator_missing_injected_parameter, + r#" +from typing import Callable, Concatenate + +def with_current_tenant_id[T, **P, R]( + view: Callable[Concatenate[T, str, P], R], +) -> Callable[Concatenate[T, P], R]: + ... + +class Foo: + @with_current_tenant_id + def get(self) -> int: # E: Function `get` is missing parameter of type `str` injected by decorator `with_current_tenant_id` + return 0 + "#, +); + testcase!( bug = "This error message is confusing, I think we need to be clearer when we are printing the *type* of an argument", test_decorator_error_message,