Skip to content
Open
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
69 changes: 68 additions & 1 deletion pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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<String> {
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,
))
Comment on lines +1638 to +1650
}

fn apply_function_decorator(
&self,
decorator: Type,
decoratee: Type,
metadata: &FuncMetadata,
decoratee_name: &Identifier,
range: TextRange,
errors: &ErrorCollector,
) -> Type {
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions pyrefly/lib/test/decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading