Skip to content

Commit 95e2f63

Browse files
Retain extra ellipses in protocols and abstract methods (#8769)
## Summary It turns out that some type checkers rely on the presence of ellipses in `Protocol` interfaces and abstract methods, in order to differentiate between default implementations and stubs. This PR modifies the preview behavior of `PIE790` to avoid flagging "unnecessary" ellipses in such cases. Closes #8756. ## Test Plan `cargo test`
1 parent 00a015c commit 95e2f63

File tree

4 files changed

+82
-0
lines changed

4 files changed

+82
-0
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_pie/PIE790.py

+30
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,33 @@ def foo():
177177
for i in range(10):
178178
...
179179
pass
180+
181+
from typing import Protocol
182+
183+
184+
class Repro(Protocol):
185+
def func(self) -> str:
186+
"""Docstring"""
187+
...
188+
189+
def impl(self) -> str:
190+
"""Docstring"""
191+
return self.func()
192+
193+
194+
import abc
195+
196+
197+
class Repro:
198+
@abc.abstractmethod
199+
def func(self) -> str:
200+
"""Docstring"""
201+
...
202+
203+
def impl(self) -> str:
204+
"""Docstring"""
205+
return self.func()
206+
207+
def stub(self) -> str:
208+
"""Docstring"""
209+
...

crates/ruff_linter/src/rules/flake8_pie/rules/unnecessary_placeholder.rs

+25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix};
33
use ruff_macros::{derive_message_formats, violation};
44
use ruff_python_ast::whitespace::trailing_comment_start_offset;
55
use ruff_python_ast::Stmt;
6+
use ruff_python_semantic::{ScopeKind, SemanticModel};
67
use ruff_text_size::Ranged;
78

89
use crate::checkers::ast::Checker;
@@ -93,6 +94,12 @@ pub(crate) fn unnecessary_placeholder(checker: &mut Checker, body: &[Stmt]) {
9394
if expr.value.is_ellipsis_literal_expr()
9495
&& checker.settings.preview.is_enabled() =>
9596
{
97+
// Ellipses are significant in protocol methods and abstract methods. Specifically,
98+
// Pyright uses the presence of an ellipsis to indicate that a method is a stub,
99+
// rather than a default implementation.
100+
if in_protocol_or_abstract_method(checker.semantic()) {
101+
return;
102+
}
96103
Placeholder::Ellipsis
97104
}
98105
_ => continue,
@@ -125,3 +132,21 @@ impl std::fmt::Display for Placeholder {
125132
}
126133
}
127134
}
135+
136+
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
137+
/// method.
138+
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
139+
semantic.current_scopes().any(|scope| match scope.kind {
140+
ScopeKind::Class(class_def) => class_def
141+
.bases()
142+
.iter()
143+
.any(|base| semantic.match_typing_expr(base, "Protocol")),
144+
ScopeKind::Function(function_def) => {
145+
ruff_python_semantic::analyze::visibility::is_abstract(
146+
&function_def.decorator_list,
147+
semantic,
148+
)
149+
}
150+
_ => false,
151+
})
152+
}

crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__PIE790_PIE790.py.snap

+5
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
473473
178 | ...
474474
179 | pass
475475
| ^^^^ PIE790
476+
180 |
477+
181 | from typing import Protocol
476478
|
477479
= help: Remove unnecessary `pass`
478480

@@ -481,5 +483,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
481483
177 177 | for i in range(10):
482484
178 178 | ...
483485
179 |- pass
486+
180 179 |
487+
181 180 | from typing import Protocol
488+
182 181 |
484489

485490

crates/ruff_linter/src/rules/flake8_pie/snapshots/ruff_linter__rules__flake8_pie__tests__preview__PIE790_PIE790.py.snap

+22
Original file line numberDiff line numberDiff line change
@@ -634,13 +634,17 @@ PIE790.py:178:5: PIE790 [*] Unnecessary `...` literal
634634
177 177 | for i in range(10):
635635
178 |- ...
636636
179 178 | pass
637+
180 179 |
638+
181 180 | from typing import Protocol
637639

638640
PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
639641
|
640642
177 | for i in range(10):
641643
178 | ...
642644
179 | pass
643645
| ^^^^ PIE790
646+
180 |
647+
181 | from typing import Protocol
644648
|
645649
= help: Remove unnecessary `pass`
646650

@@ -649,5 +653,23 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
649653
177 177 | for i in range(10):
650654
178 178 | ...
651655
179 |- pass
656+
180 179 |
657+
181 180 | from typing import Protocol
658+
182 181 |
659+
660+
PIE790.py:209:9: PIE790 [*] Unnecessary `...` literal
661+
|
662+
207 | def stub(self) -> str:
663+
208 | """Docstring"""
664+
209 | ...
665+
| ^^^ PIE790
666+
|
667+
= help: Remove unnecessary `...`
668+
669+
Safe fix
670+
206 206 |
671+
207 207 | def stub(self) -> str:
672+
208 208 | """Docstring"""
673+
209 |- ...
652674

653675

0 commit comments

Comments
 (0)