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 https://github.com/astral-sh/ruff/issues/8756.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-11-19 07:05:29 -08:00 committed by GitHub
parent 00a015ca24
commit 95e2f632e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 82 additions and 0 deletions

View File

@ -177,3 +177,33 @@ for i in range(10):
for i in range(10): for i in range(10):
... ...
pass pass
from typing import Protocol
class Repro(Protocol):
def func(self) -> str:
"""Docstring"""
...
def impl(self) -> str:
"""Docstring"""
return self.func()
import abc
class Repro:
@abc.abstractmethod
def func(self) -> str:
"""Docstring"""
...
def impl(self) -> str:
"""Docstring"""
return self.func()
def stub(self) -> str:
"""Docstring"""
...

View File

@ -3,6 +3,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::whitespace::trailing_comment_start_offset; use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::Stmt; use ruff_python_ast::Stmt;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -93,6 +94,12 @@ pub(crate) fn unnecessary_placeholder(checker: &mut Checker, body: &[Stmt]) {
if expr.value.is_ellipsis_literal_expr() if expr.value.is_ellipsis_literal_expr()
&& checker.settings.preview.is_enabled() => && checker.settings.preview.is_enabled() =>
{ {
// Ellipses are significant in protocol methods and abstract methods. Specifically,
// Pyright uses the presence of an ellipsis to indicate that a method is a stub,
// rather than a default implementation.
if in_protocol_or_abstract_method(checker.semantic()) {
return;
}
Placeholder::Ellipsis Placeholder::Ellipsis
} }
_ => continue, _ => continue,
@ -125,3 +132,21 @@ impl std::fmt::Display for Placeholder {
} }
} }
} }
/// Return `true` if the [`SemanticModel`] is in a `typing.Protocol` subclass or an abstract
/// method.
fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
semantic.current_scopes().any(|scope| match scope.kind {
ScopeKind::Class(class_def) => class_def
.bases()
.iter()
.any(|base| semantic.match_typing_expr(base, "Protocol")),
ScopeKind::Function(function_def) => {
ruff_python_semantic::analyze::visibility::is_abstract(
&function_def.decorator_list,
semantic,
)
}
_ => false,
})
}

View File

@ -473,6 +473,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
178 | ... 178 | ...
179 | pass 179 | pass
| ^^^^ PIE790 | ^^^^ PIE790
180 |
181 | from typing import Protocol
| |
= help: Remove unnecessary `pass` = help: Remove unnecessary `pass`
@ -481,5 +483,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
177 177 | for i in range(10): 177 177 | for i in range(10):
178 178 | ... 178 178 | ...
179 |- pass 179 |- pass
180 179 |
181 180 | from typing import Protocol
182 181 |

View File

@ -634,6 +634,8 @@ PIE790.py:178:5: PIE790 [*] Unnecessary `...` literal
177 177 | for i in range(10): 177 177 | for i in range(10):
178 |- ... 178 |- ...
179 178 | pass 179 178 | pass
180 179 |
181 180 | from typing import Protocol
PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
| |
@ -641,6 +643,8 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
178 | ... 178 | ...
179 | pass 179 | pass
| ^^^^ PIE790 | ^^^^ PIE790
180 |
181 | from typing import Protocol
| |
= help: Remove unnecessary `pass` = help: Remove unnecessary `pass`
@ -649,5 +653,23 @@ PIE790.py:179:5: PIE790 [*] Unnecessary `pass` statement
177 177 | for i in range(10): 177 177 | for i in range(10):
178 178 | ... 178 178 | ...
179 |- pass 179 |- pass
180 179 |
181 180 | from typing import Protocol
182 181 |
PIE790.py:209:9: PIE790 [*] Unnecessary `...` literal
|
207 | def stub(self) -> str:
208 | """Docstring"""
209 | ...
| ^^^ PIE790
|
= help: Remove unnecessary `...`
Safe fix
206 206 |
207 207 | def stub(self) -> str:
208 208 | """Docstring"""
209 |- ...