Skip to content
4 changes: 4 additions & 0 deletions pyrefly/benches/micro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}():");
}
Expand Down
37 changes: 28 additions & 9 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/abstract_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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): ...

Expand Down
81 changes: 80 additions & 1 deletion pyrefly/lib/test/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/test/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
"#,
);
Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/class_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
);
Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions pyrefly/lib/test/lsp/hover_docstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,15 @@ Docstring Result: `Test docstring`
fn attribute_itself_test() {
let code = r#"
class Foo:
def f():
def f(self):
# ^
"""Test docstring"""
"#;
let report = get_batched_lsp_operations_report(&[("main", code)], test_report_factory(code));
assert_eq!(
r#"
# main.py
3 | def f():
3 | def f(self):
^
Docstring Result: `Test docstring`
"#
Expand Down Expand Up @@ -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`
"#
Expand Down
4 changes: 2 additions & 2 deletions pyrefly/lib/test/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
);
Expand Down Expand Up @@ -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
"#,
);
Expand Down
2 changes: 1 addition & 1 deletion pyrefly/lib/test/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading