diff --git a/pyrefly/lib/alt/function.rs b/pyrefly/lib/alt/function.rs index 7e2369a435..a3fb1f0cbb 100644 --- a/pyrefly/lib/alt/function.rs +++ b/pyrefly/lib/alt/function.rs @@ -540,6 +540,33 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } else if flags.is_staticmethod { self_type = None; } + + // The `self`/`cls` receiver of a method is supplied implicitly at call time, so a + // default value on it is unreachable and almost always a mistake (e.g. `def m(self=1)`). + // `self_type` is `Some` exactly when there is an implicit receiver (instance methods, + // classmethods including `__init_subclass__`/`__class_getitem__`, properties, and + // `__new__`); it is `None` for staticmethods and top-level functions. A variadic-only + // receiver lands in `vararg`, so `.first()` here correctly yields `None`. + // See https://github.com/facebook/pyrefly/issues/3729. + if self_type.is_some() + && let Some(first) = def + .parameters + .posonlyargs + .first() + .or_else(|| def.parameters.args.first()) + && let Some(default) = &first.default + { + self.error( + errors, + default.range(), + ErrorKind::BadFunctionDefinition, + format!( + "Parameter `{}` is the `self`/`cls` parameter and cannot have a default value", + first.parameter.name + ), + ); + } + let FunctionParamsResult { params, paramspec, diff --git a/pyrefly/lib/test/mod.rs b/pyrefly/lib/test/mod.rs index 07bb498e0c..88a49c744e 100644 --- a/pyrefly/lib/test/mod.rs +++ b/pyrefly/lib/test/mod.rs @@ -66,6 +66,7 @@ mod recursive_alias; mod redundant_cast; mod returns; mod scope; +mod self_cls_default; mod semantic_syntax_errors; mod sentinel; mod shape_dsl; diff --git a/pyrefly/lib/test/self_cls_default.rs b/pyrefly/lib/test/self_cls_default.rs new file mode 100644 index 0000000000..b816df25c7 --- /dev/null +++ b/pyrefly/lib/test/self_cls_default.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use crate::testcase; + +// https://github.com/facebook/pyrefly/issues/3729: the implicit `self`/`cls` receiver of a +// method is supplied at call time, so a default value on it is unreachable and almost always a bug. + +testcase!( + test_self_param_with_default, + r#" +class C: + def m(self=1): ... # E: cannot have a default value +"#, +); + +testcase!( + test_classmethod_cls_with_default, + r#" +class C: + @classmethod + def m(cls=1): ... # E: cannot have a default value +"#, +); + +testcase!( + test_positional_only_self_with_default, + r#" +class C: + def m(self=1, /): ... # E: cannot have a default value +"#, +); + +testcase!( + test_dunder_new_cls_with_default, + r#" +class C: + def __new__(cls=1): ... # E: cannot have a default value +"#, +); + +// `__init_subclass__` is an implicit classmethod, so its `cls` receiver is also checked. +testcase!( + test_dunder_init_subclass_cls_with_default, + r#" +class C: + def __init_subclass__(cls=1): ... # E: cannot have a default value +"#, +); + +// A property is still an instance method, so its `self` receiver is checked. +testcase!( + test_property_self_with_default, + r#" +class C: + @property + def p(self=1) -> int: ... # E: cannot have a default value +"#, +); + +testcase!( + test_staticmethod_first_param_default_ok, + r#" +class C: + @staticmethod + def m(x=1): ... +"#, +); + +testcase!( + test_top_level_function_default_ok, + r#" +def f(x=1): ... +"#, +); + +testcase!( + test_non_receiver_param_default_ok, + r#" +class C: + def m(self, x=1): ... + @classmethod + def n(cls, y=2): ... +"#, +);