diff --git a/pyrefly/benches/micro.rs b/pyrefly/benches/micro.rs index 5e76cfac0e..2726ff2d72 100644 --- a/pyrefly/benches/micro.rs +++ b/pyrefly/benches/micro.rs @@ -926,6 +926,10 @@ fn nested_scopes(depth: usize, per_scope: usize, missing: usize) -> String { let pad = " ".repeat(level); if level % 4 == 3 && level + 1 < depth { let _ = writeln!(source, "{pad}class C{level}:"); + } else if level > 0 && (level - 1) % 4 == 3 { + // Directly under a class inserted above: take `self` so the + // missing-receiver check does not inflate the expected error count. + let _ = writeln!(source, "{pad}def f{level}(self):"); } else { let _ = writeln!(source, "{pad}def f{level}():"); } diff --git a/pyrefly/lib/alt/function.rs b/pyrefly/lib/alt/function.rs index 0d511d5d1a..4898e76d63 100644 --- a/pyrefly/lib/alt/function.rs +++ b/pyrefly/lib/alt/function.rs @@ -646,23 +646,42 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self_type = None; } - if defining_cls.is_some() - && flags.is_classmethod - && !flags.is_staticmethod - && flags.property_metadata.is_none() + // Methods with an implicit receiver need a positional parameter (or `*args`) + // for it. Covers classmethods, instance methods, and `__new__` (an implicit + // staticmethod that still takes `cls`). Skip `@no_type_check` for ordinary + // instance methods, properties/staticmethods other than `__new__`, and + // unresolved custom decorators that may reshape the signature. + // See https://github.com/facebook/pyrefly/issues/5295 and #5294. + let missing_receiver_param = defining_cls.is_some() && decorators.is_empty() && def.parameters.posonlyargs.is_empty() && def.parameters.args.is_empty() && def.parameters.vararg.is_none() - { + && (is_dunder_new + || (flags.is_classmethod + && !flags.is_staticmethod + && flags.property_metadata.is_none()) + || (self_type.is_some() + && !flags.is_classmethod + && !flags.is_staticmethod + && !flags.has_no_type_check)); + if missing_receiver_param { + let message = if flags.is_classmethod || is_dunder_new { + format!( + "Class method `{}` must accept the class as its first positional parameter", + def.name + ) + } else { + format!( + "Instance method `{}` must accept a positional argument", + def.name + ) + }; self.error( errors, def.name.range(), ErrorKind::BadFunctionDefinition, - format!( - "Class method `{}` must accept the class as its first positional parameter", - def.name - ), + message, ); } diff --git a/pyrefly/lib/test/abstract_methods.rs b/pyrefly/lib/test/abstract_methods.rs index f405834708..650bfbb5eb 100644 --- a/pyrefly/lib/test/abstract_methods.rs +++ b/pyrefly/lib/test/abstract_methods.rs @@ -809,11 +809,11 @@ from abc import ABC, abstractmethod class A1(ABC): @abstractmethod - def a(): ... + def a(): ... # E: Instance method `a` must accept a positional argument class B1(ABC): @abstractmethod - def b(): ... + def b(): ... # E: Instance method `b` must accept a positional argument class C(A1, B1): ... diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index bdf016bed3..912640f50d 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -1412,12 +1412,91 @@ testcase!( test_missing_self_parameter, r#" class C: - def f(): + def f(): # E: Instance method `f` must accept a positional argument pass C().f() # E: Expected 0 positional arguments, got 1 (including implicit `self`) "#, ); +// https://github.com/facebook/pyrefly/issues/5295 +testcase!( + test_instance_method_missing_receiver, + r#" +class Foo: + def fn() -> None: ... # E: Instance method `fn` must accept a positional argument + + def kwargs_only(**kwargs: object) -> None: ... # E: Instance method `kwargs_only` must accept a positional argument + + def keyword_only(*, x: object = None) -> None: ... # E: Instance method `keyword_only` must accept a positional argument + + def conventional(self) -> None: ... + + def unconventional(this) -> None: ... + + def positional_only(this, /) -> None: ... + + def variadic(*args: object) -> None: ... + + @staticmethod + def static() -> None: ... + + def outer(self) -> None: + def inner() -> None: ... + inner() + +Foo().conventional() +Foo().unconventional() +Foo().positional_only() +Foo().variadic(1) +Foo.static() + "#, +); + +testcase!( + test_new_missing_cls, + r#" +class A: + def __new__() -> "A": ... # E: Class method `__new__` must accept the class as its first positional parameter + +class B: + def __new__(cls) -> "B": ... + +class C: + def __new__(*args: object) -> "C": ... + "#, +); + +testcase!( + test_instance_method_missing_receiver_decorators, + r#" +from typing import Any, Callable, final +from abc import ABC, abstractmethod + +def add_receiver(f: Callable[[], None]) -> Callable[[object], None]: + def wrapper(self: object) -> None: + f() + return wrapper + +unknown: Any = lambda f: f + +class C(ABC): + @final + def plain() -> None: ... # E: Instance method `plain` must accept a positional argument + + @abstractmethod + def abstract() -> None: ... # E: Instance method `abstract` must accept a positional argument + + @add_receiver + def wrapped() -> None: ... + + @unknown + def uncertain() -> None: ... + + @property + def prop() -> int: ... # E: Instance method `prop` must accept a positional argument + "#, +); + testcase!( test_generic_instance_method, r#" diff --git a/pyrefly/lib/test/callable.rs b/pyrefly/lib/test/callable.rs index 17b60f3c4d..2e28303a68 100644 --- a/pyrefly/lib/test/callable.rs +++ b/pyrefly/lib/test/callable.rs @@ -518,7 +518,7 @@ testcase!( test_missing_self_and_kwonly, r#" class A: - def f(*, x): ... + def f(*, x): ... # E: Instance method `f` must accept a positional argument A().f(1) # E: Expected argument `x` to be passed by name # E: Expected 0 positional arguments, got 2 (including implicit `self`) "#, ); diff --git a/pyrefly/lib/test/class_overrides.rs b/pyrefly/lib/test/class_overrides.rs index 0529a7224c..0693528db0 100644 --- a/pyrefly/lib/test/class_overrides.rs +++ b/pyrefly/lib/test/class_overrides.rs @@ -2434,11 +2434,11 @@ testcase!( test_override_method_without_self, r#" class A: - def foo() -> int: + def foo() -> int: # E: Instance method `foo` must accept a positional argument return 42 class B(A): - def foo() -> int: + def foo() -> int: # E: Instance method `foo` must accept a positional argument return 100 "#, ); diff --git a/pyrefly/lib/test/constructors.rs b/pyrefly/lib/test/constructors.rs index 0e9e235e9d..68ccbaa045 100644 --- a/pyrefly/lib/test/constructors.rs +++ b/pyrefly/lib/test/constructors.rs @@ -852,9 +852,9 @@ from typing import overload class C: @overload - def __init__(): ... # E: Overloaded function must have an implementation + def __init__(): ... # E: Overloaded function must have an implementation # E: Instance method `__init__` must accept a positional argument @overload - def __init__(): ... + def __init__(): ... # E: Instance method `__init__` must accept a positional argument C() # E: This method has no `self` parameter to receive the implicit instance argument "#, diff --git a/pyrefly/lib/test/descriptors.rs b/pyrefly/lib/test/descriptors.rs index 68e4a287a8..88e87f3130 100644 --- a/pyrefly/lib/test/descriptors.rs +++ b/pyrefly/lib/test/descriptors.rs @@ -1295,7 +1295,7 @@ testcase!( test_self_referential_descriptor_get_no_crash, r#" class C: - def d() -> C: ... + def d() -> C: ... # E: Instance method `d` must accept a positional argument @d # E: Expected 0 positional arguments, got 1 in function `C.d` def __get__(): pass @@ -1454,7 +1454,7 @@ testcase!( test_self_referential_descriptor_set_no_crash, r#" class C: - def d() -> C: ... + def d() -> C: ... # E: Instance method `d` must accept a positional argument @d # E: Expected 0 positional arguments, got 1 in function `C.d` def __get__(): pass diff --git a/pyrefly/lib/test/lsp/hover_docstring.rs b/pyrefly/lib/test/lsp/hover_docstring.rs index 5695fbe07d..f95d407a93 100644 --- a/pyrefly/lib/test/lsp/hover_docstring.rs +++ b/pyrefly/lib/test/lsp/hover_docstring.rs @@ -152,7 +152,7 @@ Docstring Result: `Test docstring` fn attribute_itself_test() { let code = r#" class Foo: - def f(): + def f(self): # ^ """Test docstring""" "#; @@ -160,7 +160,7 @@ class Foo: assert_eq!( r#" # main.py -3 | def f(): +3 | def f(self): ^ Docstring Result: `Test docstring` "# @@ -195,16 +195,16 @@ Docstring Result: `Test docstring` fn attribute_test() { let code = r#" class Foo: - def f(): + def f(self): """Test docstring""" -print(Foo.f()) +print(Foo.f) # ^ "#; let report = get_batched_lsp_operations_report(&[("main", code)], test_report_factory(code)); assert_eq!( r#" # main.py -5 | print(Foo.f()) +5 | print(Foo.f) ^ Docstring Result: `Test docstring` "# diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index f4c94f02f0..6751c24790 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -3164,7 +3164,7 @@ class C: return True class D: - def guard_missing_self() -> TypeGuard[int]: # E: Type guard functions must accept at least one positional argument + def guard_missing_self() -> TypeGuard[int]: # E: Type guard functions must accept at least one positional argument # E: Instance method `guard_missing_self` must accept a positional argument return True "#, ); @@ -3232,7 +3232,7 @@ class C: return True class D: - def guard_missing_self() -> TypeIs[int]: # E: Type guard functions must accept at least one positional argument + def guard_missing_self() -> TypeIs[int]: # E: Type guard functions must accept at least one positional argument # E: Instance method `guard_missing_self` must accept a positional argument return True "#, ); diff --git a/pyrefly/lib/test/scope.rs b/pyrefly/lib/test/scope.rs index d145fb3afc..2e6a836f6e 100644 --- a/pyrefly/lib/test/scope.rs +++ b/pyrefly/lib/test/scope.rs @@ -1409,7 +1409,7 @@ from typing import assert_type, Literal x = 'string' class A: x = 42 - def f(): + def f(self): assert_type(x, str) lambda_f = lambda: assert_type(x, str) class B: