[flake8-pyi] Implement PYI056 (#5959)

## Summary

Checks that `append`, `extend` and `remove` methods are not called on
`__all__`. See [original
implementation](2a86db8271/pyi.py (L1133-L1138)).

```
$ flake8 --select Y026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi

crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:3:1: Y056 Calling ".append()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:4:1: Y056 Calling ".extend()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:5:1: Y056 Calling ".remove()" on "__all__" may not be supported by all type checkers (use += instead)
```

```
$ ./target/debug/ruff --select PYI026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi --no-cache

crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:3:1: PYI056 Calling ".append()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:4:1: PYI056 Calling ".extend()" on "__all__" may not be supported by all type checkers (use += instead)
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi:5:1: PYI056 Calling ".remove()" on "__all__" may not be supported by all type checkers (use += instead)
Found 3 errors.
```

ref #848

## Test Plan

Snapshots and manual runs of flake8.
This commit is contained in:
Victor Hugo Gomes 2023-07-22 01:25:54 -03:00 committed by GitHub
parent 45318d08b7
commit 33657d3a1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 0 deletions

View file

@ -0,0 +1,12 @@
__all__ = ["A", "B", "C"]
# Errors
__all__.append("D")
__all__.extend(["E", "Foo"])
__all__.remove("A")
# OK
__all__ += ["D"]
foo = ["Hello"]
foo.append("World")
foo.bar.append("World")

View file

@ -0,0 +1,12 @@
__all__ = ["A", "B", "C"]
# Errors
__all__.append("D")
__all__.extend(["E", "Foo"])
__all__.remove("A")
# OK
__all__ += ["D"]
foo = ["Hello"]
foo.append("World")
foo.bar.append("World")

View file

@ -3037,6 +3037,9 @@ where
if self.enabled(Rule::DjangoLocalsInRenderFunction) {
flake8_django::rules::locals_in_render_function(self, func, args, keywords);
}
if self.is_stub && self.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(self, func);
}
}
Expr::Dict(ast::ExprDict {
keys,

View file

@ -652,6 +652,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub),
(Flake8Pyi, "054") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NumericLiteralTooLong),
(Flake8Pyi, "053") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StringOrBytesTooLong),
(Flake8Pyi, "056") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
// flake8-pytest-style
(Flake8PytestStyle, "001") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),

View file

@ -91,6 +91,8 @@ mod tests {
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
#[test_case(Rule::UnsupportedMethodCallOnAll, Path::new("PYI056.py"))]
#[test_case(Rule::UnsupportedMethodCallOnAll, Path::new("PYI056.pyi"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View file

@ -28,6 +28,7 @@ pub(crate) use unaliased_collections_abc_set_import::*;
pub(crate) use unnecessary_literal_union::*;
pub(crate) use unrecognized_platform::*;
pub(crate) use unrecognized_version_info::*;
pub(crate) use unsupported_method_call_on_all::*;
mod any_eq_ne_annotation;
mod bad_version_info_comparison;
@ -59,3 +60,4 @@ mod unaliased_collections_abc_set_import;
mod unnecessary_literal_union;
mod unrecognized_platform;
mod unrecognized_version_info;
mod unsupported_method_call_on_all;

View file

@ -0,0 +1,65 @@
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that `append`, `extend` and `remove` methods are not called on
/// `__all__`.
///
/// ## Why is this bad?
/// Different type checkers have varying levels of support for calling these
/// methods on `__all__`. Instead, use the `+=` operator to add items to
/// `__all__`, which is known to be supported by all major type checkers.
///
/// ## Example
/// ```python
/// __all__ = ["A"]
/// __all__.append("B")
/// ```
///
/// Use instead:
/// ```python
/// __all__ = ["A"]
/// __all__ += "B"
/// ```
#[violation]
pub struct UnsupportedMethodCallOnAll {
name: String,
}
impl Violation for UnsupportedMethodCallOnAll {
#[derive_message_formats]
fn message(&self) -> String {
let UnsupportedMethodCallOnAll { name } = self;
format!("Calling `.{name}()` on `__all__` may not be supported by all type checkers (use `+=` instead)")
}
}
/// PYI056
pub(crate) fn unsupported_method_call_on_all(checker: &mut Checker, func: &Expr) {
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func else {
return;
};
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id.as_str() != "__all__" {
return;
}
if !is_unsupported_method(attr.as_str()) {
return;
}
checker.diagnostics.push(Diagnostic::new(
UnsupportedMethodCallOnAll {
name: attr.to_string(),
},
func.range(),
));
}
fn is_unsupported_method(name: &str) -> bool {
matches!(name, "append" | "extend" | "remove")
}

View file

@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View file

@ -0,0 +1,32 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI056.pyi:4:1: PYI056 Calling `.append()` on `__all__` may not be supported by all type checkers (use `+=` instead)
|
3 | # Errors
4 | __all__.append("D")
| ^^^^^^^^^^^^^^ PYI056
5 | __all__.extend(["E", "Foo"])
6 | __all__.remove("A")
|
PYI056.pyi:5:1: PYI056 Calling `.extend()` on `__all__` may not be supported by all type checkers (use `+=` instead)
|
3 | # Errors
4 | __all__.append("D")
5 | __all__.extend(["E", "Foo"])
| ^^^^^^^^^^^^^^ PYI056
6 | __all__.remove("A")
|
PYI056.pyi:6:1: PYI056 Calling `.remove()` on `__all__` may not be supported by all type checkers (use `+=` instead)
|
4 | __all__.append("D")
5 | __all__.extend(["E", "Foo"])
6 | __all__.remove("A")
| ^^^^^^^^^^^^^^ PYI056
7 |
8 | # OK
|

1
ruff.schema.json generated
View file

@ -2380,6 +2380,7 @@
"PYI052",
"PYI053",
"PYI054",
"PYI056",
"Q",
"Q0",
"Q00",