Compare commits

...

22 Commits

Author SHA1 Message Date
Charlie Marsh
1c3265ef98 Bump version to 0.0.231 2023-01-23 12:51:09 -05:00
Maksudul Haque
8001a1639c [flake8-bandit] Added Rule S612 (Use of insecure logging.config.listen) (#2108)
ref: https://github.com/charliermarsh/ruff/issues/1646
2023-01-23 12:37:33 -05:00
Charlie Marsh
7d9c1d7a5a Add a note on some isort incompatibilities 2023-01-23 12:32:35 -05:00
Thomas MK
c5cebb106e Fix outdated description of ruff's support of isort settings (#2106)
Ruff supports more than `known-first-party`, `known-third-party`, `extra-standard-library`, and `src` nowadays.

Not sure if this is the best wording. Suggestions welcome!
2023-01-23 12:29:44 -05:00
Martin Fischer
8c61e8a1ef Improve #[derive(RuleNamespace)] error handling 2023-01-23 12:20:10 -05:00
Martin Fischer
4f338273a5 refactor: Simplify test_ruff_black_compatibility 2023-01-23 12:20:10 -05:00
Martin Fischer
648191652d refactor: Collect into Result<Vec<_>, _> 2023-01-23 12:20:10 -05:00
Martin Fischer
90558609c3 refactor: Introduce parse_doc_attr helper function 2023-01-23 12:20:10 -05:00
Martin Fischer
991d3c1ef6 refactor: Move Linter::url and Linter::name generation to proc macro
This lets us get rid of the build.rs script and results
in more developer-friendly compile error messages.
2023-01-23 12:20:10 -05:00
Simon Brugman
f472fbc6d4 docs(readme): add pypa cibuildwheel (#2107) 2023-01-23 11:39:23 -05:00
Charlie Marsh
09b65a6449 Remove some usages of default format for expressions (#2100) 2023-01-22 23:15:43 -05:00
Charlie Marsh
9d2eced941 Add flake8-simplify to CONTRIBUTING.md 2023-01-22 21:46:52 -05:00
Charlie Marsh
be0f6acb40 Change contributing to point to tryceratops 2023-01-22 21:45:20 -05:00
Steve Dignam
0c624af036 Add flake8-pie PIE800: no-unnecessary-spread (#1881)
Checks for unnecessary spreads, like `{**foo, **{"bar": True}}`
rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:43:34 -05:00
Steve Dignam
4ca328f964 Add flake8-pie PIE804: no-unnecessary-dict-kwargs (#1884)
Warn about things like `foo(**{"bar": True})` which is equivalent to `foo(bar=True)`

rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:32:45 -05:00
Charlie Marsh
07b5bf7030 Remove misleading emoji comment 2023-01-22 21:23:55 -05:00
Charlie Marsh
f40ae943a7 Fix bad documentation message for init option 2023-01-22 19:25:23 -05:00
Charlie Marsh
8d46d3bfa6 Avoid nested-if violations when outer-if has else clause (#2095)
It looks like we need `do`-`while`-like semantics here with an additional outer check.

Closes #2094.
2023-01-22 17:40:56 -05:00
alm
4fb0c6e3ad feat: Implement TRY201 (#2073) 2023-01-22 17:08:57 -05:00
Simon Brugman
ebfdefd110 refactor: remove redundant enum (#2091) 2023-01-22 15:27:08 -05:00
Simon Brugman
11f06055a0 feat: flake8-use-pathlib PTH100-124 (#2090) 2023-01-22 15:17:25 -05:00
Ville Skyttä
6a6a792562 fix: issue D401 only for non-test/property functions and methods (#2071)
Extend test fixture to verify the targeting.

Includes two "attribute docstrings" which per PEP 257 are not recognized by the Python bytecode compiler or available as runtime object attributes. They are not available for us either at time of writing, but include them for completeness anyway in case they one day are.
2023-01-22 14:24:59 -05:00
67 changed files with 2578 additions and 190 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.230
rev: v0.0.231
hooks:
- id: ruff

View File

@@ -14,9 +14,10 @@ If you're looking for a place to start, we recommend implementing a new lint rul
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules in [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998),
and looking to the originating [Python source](https://github.com/MartinThoma/flake8-simplify) for
guidance.
As a concrete example: consider taking on one of the rules from the [`tryceratops`](https://github.com/charliermarsh/ruff/issues/2056)
plugin, and looking to the originating [Python source](https://github.com/guilatrova/tryceratops)
for guidance. [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998) has a few rules
left too.
### Prerequisites

10
Cargo.lock generated
View File

@@ -719,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1828,7 +1828,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"anyhow",
"bitflags",
@@ -1882,7 +1882,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1919,7 +1919,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1940,7 +1940,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"once_cell",
"proc-macro2",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.230"
version = "0.0.231"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,7 +46,7 @@ num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.230", path = "ruff_macros" }
ruff_macros = { version = "0.0.231", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }

View File

@@ -63,6 +63,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [SnowCLI (Snowflake)](https://github.com/Snowflake-Labs/snowcli)
- [cibuildwheel](https://github.com/pypa/cibuildwheel)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -141,6 +142,7 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-type-checking (TYP)](#flake8-type-checking-typ)
1. [tryceratops (TRY)](#tryceratops-try)
1. [flake8-use-pathlib (PTH)](#flake8-use-pathlib-pth)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
@@ -201,7 +203,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.230'
rev: 'v0.0.231'
hooks:
- id: ruff
```
@@ -810,6 +812,7 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| S506 | unsafe-yaml-load | Probable use of unsafe loader `{name}` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
| S508 | snmp-insecure-version | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
| S509 | snmp-weak-cryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
| S612 | logging-config-insecure-listen | Use of insecure `logging.config.listen` detected | |
| S701 | jinja2-autoescape-false | Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function. | |
### flake8-blind-except (BLE)
@@ -1150,7 +1153,9 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE807 | prefer-list-builtin | Prefer `list()` over useless lambda | 🛠 |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
### flake8-commas (COM)
@@ -1195,8 +1200,41 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY201 | verbose-raise | Use `raise` without specifying exception name | |
| TRY300 | try-consider-else | Consider `else` block | |
### flake8-use-pathlib (PTH)
For more, see [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PTH100 | pathlib-abspath | `os.path.abspath` should be replaced by `.resolve()` | |
| PTH101 | pathlib-chmod | `os.chmod` should be replaced by `.chmod()` | |
| PTH102 | pathlib-mkdir | `os.mkdir` should be replaced by `.mkdir()` | |
| PTH103 | pathlib-makedirs | `os.makedirs` should be replaced by `.mkdir(parents=True)` | |
| PTH104 | pathlib-rename | `os.rename` should be replaced by `.rename()` | |
| PTH105 | pathlib-replace | `os.replace`should be replaced by `.replace()` | |
| PTH106 | pathlib-rmdir | `os.rmdir` should be replaced by `.rmdir()` | |
| PTH107 | pathlib-remove | `os.remove` should be replaced by `.unlink()` | |
| PTH108 | pathlib-unlink | `os.unlink` should be replaced by `.unlink()` | |
| PTH109 | pathlib-getcwd | `os.getcwd()` should be replaced by `Path.cwd()` | |
| PTH110 | pathlib-exists | `os.path.exists` should be replaced by `.exists()` | |
| PTH111 | pathlib-expanduser | `os.path.expanduser` should be replaced by `.expanduser()` | |
| PTH112 | pathlib-is-dir | `os.path.isdir` should be replaced by `.is_dir()` | |
| PTH113 | pathlib-is-file | `os.path.isfile` should be replaced by `.is_file()` | |
| PTH114 | pathlib-is-link | `os.path.islink` should be replaced by `.is_symlink()` | |
| PTH115 | pathlib-readlink | `os.readlink(` should be replaced by `.readlink()` | |
| PTH116 | pathlib-stat | `os.stat` should be replaced by `.stat()` or `.owner()` or `.group()` | |
| PTH117 | pathlib-is-abs | `os.path.isabs` should be replaced by `.is_absolute()` | |
| PTH118 | pathlib-join | `os.path.join` should be replaced by foo_path / "bar" | |
| PTH119 | pathlib-basename | `os.path.basename` should be replaced by `.name` | |
| PTH120 | pathlib-dirname | `os.path.dirname` should be replaced by `.parent` | |
| PTH121 | pathlib-samefile | `os.path.samefile` should be replaced by `.samefile()` | |
| PTH122 | pathlib-splitext | `os.path.splitext` should be replaced by `.suffix` | |
| PTH123 | pathlib-open | `open("foo")` should be replaced by`Path("foo").open()` | |
| PTH124 | pathlib-py-path | `py.path` is in maintenance mode, use `pathlib` instead | |
### Ruff-specific rules (RUF)
| Code | Name | Message | Fix |
@@ -1602,12 +1640,15 @@ project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be nearly equivalent to `isort` when used `profile = "black"`.
(There are some minor differences in how Ruff and isort break ties between similar imports.)
There are a few known, minor differences in how Ruff and isort break ties between similar imports,
and in how Ruff and isort treat inline comments in some cases (see: [#1381](https://github.com/charliermarsh/ruff/issues/1381),
[#2104](https://github.com/charliermarsh/ruff/issues/2104)).
Like `isort`, Ruff's import sorting is compatible with Black.
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
`extra-standard-library`, and `src` settings, like so:
Ruff does not yet support all of `isort`'s configuration options, though it does support many of
them. You can find the supported settings in the [API reference](#isort). For example, you can set
`known-first-party` like so:
```toml
[tool.ruff]
@@ -2187,10 +2228,9 @@ ignore = ["F841"]
#### [`ignore-init-module-imports`](#ignore-init-module-imports)
Avoid automatically removing unused imports in `__init__.py` files. Such
imports will still be +flagged, but with a dedicated message
suggesting that the import is either added to the module' +`__all__`
symbol, or re-exported with a redundant alias (e.g., `import os as
os`).
imports will still be flagged, but with a dedicated message suggesting
that the import is either added to the module's `__all__` symbol, or
re-exported with a redundant alias (e.g., `import os as os`).
**Default value**: `false`

View File

@@ -1,84 +0,0 @@
use std::fs;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
fn main() {
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
generate_linter_name_and_url(&out_dir);
}
const RULES_SUBMODULE_DOC_PREFIX: &str = "//! Rules from ";
/// The `src/rules/*/mod.rs` files are expected to have a first line such as the
/// following:
///
/// //! Rules from [Pyflakes](https://pypi.org/project/pyflakes/).
///
/// This function extracts the link label and url from these comments and
/// generates the `name` and `url` functions for the `Linter` enum
/// accordingly, so that they can be used by `ruff_dev::generate_rules_table`.
fn generate_linter_name_and_url(out_dir: &Path) {
println!("cargo:rerun-if-changed=src/rules/");
let mut name_match_arms: String = r#"Linter::Ruff => "Ruff-specific rules","#.into();
let mut url_match_arms: String = r#"Linter::Ruff => None,"#.into();
for file in fs::read_dir("src/rules/")
.unwrap()
.flatten()
.filter(|f| f.file_type().unwrap().is_dir() && f.file_name() != "ruff")
{
let mod_rs_path = file.path().join("mod.rs");
let mod_rs_path = mod_rs_path.to_str().unwrap();
let first_line = BufReader::new(fs::File::open(mod_rs_path).unwrap())
.lines()
.next()
.unwrap()
.unwrap();
let Some(comment) = first_line.strip_prefix(RULES_SUBMODULE_DOC_PREFIX) else {
panic!("expected first line in {mod_rs_path} to start with `{RULES_SUBMODULE_DOC_PREFIX}`")
};
let md_link = comment.trim_end_matches('.');
let (name, url) = md_link
.strip_prefix('[')
.unwrap()
.strip_suffix(')')
.unwrap()
.split_once("](")
.unwrap();
let dirname = file.file_name();
let dirname = dirname.to_str().unwrap();
let variant_name = dirname
.split('_')
.map(|part| match part {
"errmsg" => "ErrMsg".to_string(),
"mccabe" => "McCabe".to_string(),
"pep8" => "PEP8".to_string(),
_ => format!("{}{}", part[..1].to_uppercase(), &part[1..]),
})
.collect::<String>();
name_match_arms.push_str(&format!(r#"Linter::{variant_name} => "{name}","#));
url_match_arms.push_str(&format!(r#"Linter::{variant_name} => Some("{url}"),"#));
}
write!(
BufWriter::new(fs::File::create(out_dir.join("linter.rs")).unwrap()),
"
impl Linter {{
pub fn name(&self) -> &'static str {{
match self {{ {name_match_arms} }}
}}
pub fn url(&self) -> Option<&'static str> {{
match self {{ {url_match_arms} }}
}}
}}
"
)
.unwrap();
}

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.230"
version = "0.0.231"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.230"
version = "0.0.231"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Rodolphe Pelloux-Prayer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.230"
version = "0.0.231"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -0,0 +1,8 @@
import logging.config
t = logging.config.listen(9999)
def verify_func():
pass
l = logging.config.listen(9999, verify=verify_func)

View File

@@ -0,0 +1,17 @@
{"foo": 1, **{"bar": 1}} # PIE800
foo({**foo, **{"bar": True}}) # PIE800
{**foo, **{"bar": 10}} # PIE800
{**foo, **buzz, **{bar: 10}} # PIE800
{**foo, "bar": True } # OK
{"foo": 1, "buzz": {"bar": 1}} # OK
{**foo, "bar": True } # OK
Table.objects.filter(inst=inst, **{f"foo__{bar}__exists": True}) # OK
buzz = {**foo, "bar": { 1: 2 }} # OK

View File

@@ -0,0 +1,19 @@
foo(**{"bar": True}) # PIE804
foo(**{"r2d2": True}) # PIE804
Foo.objects.create(**{"bar": True}) # PIE804
Foo.objects.create(**{"_id": some_id}) # PIE804
Foo.objects.create(**{**bar}) # PIE804
foo(**{**data, "foo": "buzz"})
foo(**buzz)
foo(**{"bar-foo": True})
foo(**{"bar foo": True})
foo(**{"1foo": True})
foo(**{buzz: True})
foo(**{"": True})
foo(**{f"buzz__{bar}": True})

View File

@@ -92,3 +92,40 @@ if node.module:
"multiprocessing."
):
print("Bad module!")
# OK
if a:
if b:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
else:
print("bar")
else:
print("bar")
# OK
if a:
# SIM 102
if b:
if c:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
print("baz")
else:
print("bar")

View File

@@ -0,0 +1,31 @@
import os
import os.path
p = "/foo"
a = os.path.abspath(p)
aa = os.chmod(p)
aaa = os.mkdir(p)
os.makedirs(p)
os.rename(p)
os.replace(p)
os.rmdir(p)
os.remove(p)
os.unlink(p)
os.getcwd(p)
b = os.path.exists(p)
bb = os.path.expanduser(p)
bbb = os.path.isdir(p)
bbbb = os.path.isfile(p)
bbbbb = os.path.islink(p)
os.readlink(p)
os.stat(p)
os.path.isabs(p)
os.path.join(p)
os.path.basename(p)
os.path.dirname(p)
os.path.samefile(p)
os.path.splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,28 @@
import os as foo
import os.path as foo_p
p = "/foo"
a = foo_p.abspath(p)
aa = foo.chmod(p)
aaa = foo.mkdir(p)
foo.makedirs(p)
foo.rename(p)
foo.replace(p)
foo.rmdir(p)
foo.remove(p)
foo.unlink(p)
foo.getcwd(p)
b = foo_p.exists(p)
bb = foo_p.expanduser(p)
bbb = foo_p.isdir(p)
bbbb = foo_p.isfile(p)
bbbbb = foo_p.islink(p)
foo.readlink(p)
foo.stat(p)
foo_p.isabs(p)
foo_p.join(p)
foo_p.basename(p)
foo_p.dirname(p)
foo_p.samefile(p)
foo_p.splitext(p)

View File

@@ -0,0 +1,33 @@
from os import chmod, mkdir, makedirs, rename, replace, rmdir
from os import remove, unlink, getcwd, readlink, stat
from os.path import abspath, exists, expanduser, isdir, isfile, islink
from os.path import isabs, join, basename, dirname, samefile, splitext
p = "/foo"
a = abspath(p)
aa = chmod(p)
aaa = mkdir(p)
makedirs(p)
rename(p)
replace(p)
rmdir(p)
remove(p)
unlink(p)
getcwd(p)
b = exists(p)
bb = expanduser(p)
bbb = isdir(p)
bbbb = isfile(p)
bbbbb = islink(p)
readlink(p)
stat(p)
isabs(p)
join(p)
basename(p)
dirname(p)
samefile(p)
splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,35 @@
from os import chmod as xchmod, mkdir as xmkdir
from os import makedirs as xmakedirs, rename as xrename, replace as xreplace
from os import rmdir as xrmdir, remove as xremove, unlink as xunlink
from os import getcwd as xgetcwd, readlink as xreadlink, stat as xstat
from os.path import abspath as xabspath, exists as xexists
from os.path import expanduser as xexpanduser, isdir as xisdir
from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
from os.path import join as xjoin, basename as xbasename, dirname as xdirname
from os.path import samefile as xsamefile, splitext as xsplitext
p = "/foo"
a = xabspath(p)
aa = xchmod(p)
aaa = xmkdir(p)
xmakedirs(p)
xrename(p)
xreplace(p)
xrmdir(p)
xremove(p)
xunlink(p)
xgetcwd(p)
b = xexists(p)
bb = xexpanduser(p)
bbb = xisdir(p)
bbbb = xisfile(p)
bbbbb = xislink(p)
xreadlink(p)
xstat(p)
xisabs(p)
xjoin(p)
xbasename(p)
xdirname(p)
xsamefile(p)
xsplitext(p)

View File

@@ -0,0 +1,3 @@
import py
p = py.path.local("../foo")

View File

@@ -0,0 +1,3 @@
from py.path import local as path
p = path("/foo")

View File

@@ -1,3 +1,7 @@
"""This module docstring does not need to be written in imperative mood."""
from functools import cached_property
# Bad examples
def bad_liouiwnlkjl():
@@ -18,7 +22,11 @@ def bad_sdgfsdg23245777():
def bad_run_something():
"""Runs something"""
pass
def bad_nested():
"""Runs other things, nested"""
bad_nested()
def multi_line():
@@ -32,6 +40,11 @@ def multi_line():
def good_run_something():
"""Run away."""
def good_nested():
"""Run to the hills."""
good_nested()
def good_construct():
"""Construct a beautiful house."""
@@ -41,3 +54,48 @@ def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""
good_top_level_var = False
"""This top level assignment attribute docstring does not need to be written in imperative mood."""
# Classes and their members
class Thingy:
"""This class docstring does not need to be written in imperative mood."""
_beep = "boop"
"""This class attribute docstring does not need to be written in imperative mood."""
def bad_method(self):
"""This method docstring should be written in imperative mood."""
@property
def good_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return self._beep
@cached_property
def good_cached_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return 42 * 42
class NestedThingy:
"""This nested class docstring does not need to be written in imperative mood."""
# Test functions
def test_something():
"""This test function does not need to be written in imperative mood.
pydocstyle's rationale:
We exclude tests from the imperative mood check, because to phrase
their docstring in the imperative mood, they would have to start with
a highly redundant "Test that ..."
"""
def runTest():
"""This test function does not need to be written in imperative mood, either."""

View File

@@ -0,0 +1,54 @@
"""
Violation:
Raising an exception using its assigned name is verbose and unrequired
"""
import logging
logger = logging.getLogger(__name__)
class MyException(Exception):
pass
def bad():
try:
process()
except MyException as e:
logger.exception("process failed")
raise e
def good():
try:
process()
except MyException:
logger.exception("process failed")
raise
def still_good():
try:
process()
except MyException as e:
print(e)
raise
def bad_that_needs_recursion():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
raise e
def bad_that_needs_recursion_2():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
def foo():
raise e

View File

@@ -259,7 +259,7 @@
}
},
"ignore-init-module-imports": {
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be +flagged, but with a dedicated message suggesting that the import is either added to the module' +`__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be flagged, but with a dedicated message suggesting that the import is either added to the module's `__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"type": [
"boolean",
"null"
@@ -1553,6 +1553,8 @@
"PIE796",
"PIE8",
"PIE80",
"PIE800",
"PIE804",
"PIE807",
"PL",
"PLC",
@@ -1633,6 +1635,36 @@
"PT024",
"PT025",
"PT026",
"PTH",
"PTH1",
"PTH10",
"PTH100",
"PTH101",
"PTH102",
"PTH103",
"PTH104",
"PTH105",
"PTH106",
"PTH107",
"PTH108",
"PTH109",
"PTH11",
"PTH110",
"PTH111",
"PTH112",
"PTH113",
"PTH114",
"PTH115",
"PTH116",
"PTH117",
"PTH118",
"PTH119",
"PTH12",
"PTH120",
"PTH121",
"PTH122",
"PTH123",
"PTH124",
"Q",
"Q0",
"Q00",
@@ -1695,6 +1727,9 @@
"S506",
"S508",
"S509",
"S6",
"S61",
"S612",
"S7",
"S70",
"S701",
@@ -1752,6 +1787,9 @@
"TRY0",
"TRY00",
"TRY004",
"TRY2",
"TRY20",
"TRY201",
"TRY3",
"TRY30",
"TRY300",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.230"
version = "0.0.231"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"

View File

@@ -10,11 +10,8 @@ use std::{fs, process, str};
use anyhow::{anyhow, Context, Result};
use assert_cmd::Command;
use itertools::Itertools;
use log::info;
use ruff::logging::{set_up_logging, LogLevel};
use ruff::registry::{Linter, RuleNamespace};
use strum::IntoEnumIterator;
use walkdir::WalkDir;
/// Handles `blackd` process and allows submitting code to it for formatting.
@@ -175,13 +172,6 @@ fn test_ruff_black_compatibility() -> Result<()> {
.filter_map(Result::ok)
.collect();
let codes = Linter::iter()
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
.filter(|linter| *linter != Linter::Ruff)
.map(|linter| linter.prefixes().join(","))
.join(",");
let ruff_args = [
"-",
"--silent",
@@ -189,8 +179,11 @@ fn test_ruff_black_compatibility() -> Result<()> {
"--fix",
"--line-length",
"88",
"--select",
&codes,
"--select ALL",
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
"--ignore RUF",
];
for entry in paths {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.230"
version = "0.0.231"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.230"
version = "0.0.231"
edition = "2021"
[lib]

View File

@@ -1,7 +1,9 @@
use std::collections::HashSet;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DataEnum, DeriveInput, Error, Lit, Meta, MetaNameValue};
use syn::{Attribute, Data, DataEnum, DeriveInput, Error, Lit, Meta, MetaNameValue};
pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data: Data::Enum(DataEnum {
@@ -13,39 +15,62 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let mut parsed = Vec::new();
let mut prefix_match_arms = quote!();
let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules",);
let mut url_match_arms = quote!(Self::Ruff => None,);
let mut all_prefixes = HashSet::new();
for variant in variants {
let prefix_attrs: Vec<_> = variant
let prefixes: Result<Vec<_>, _> = variant
.attrs
.iter()
.filter(|a| a.path.is_ident("prefix"))
.map(|attr| {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else {
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#));
};
let str = lit.value();
match str.chars().next() {
None => return Err(Error::new(lit.span(), "expected prefix string to be non-empty")),
Some(_) => {},
}
if !all_prefixes.insert(str.clone()) {
return Err(Error::new(lit.span(), "prefix has already been defined before"));
}
Ok(str)
})
.collect();
let prefixes = prefixes?;
if prefix_attrs.is_empty() {
if prefixes.is_empty() {
return Err(Error::new(
variant.span(),
r#"Missing [#prefix = "..."] attribute"#,
r#"Missing #[prefix = "..."] attribute"#,
));
}
let mut prefix_literals = Vec::new();
for attr in prefix_attrs {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else {
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#))
};
parsed.push((lit.clone(), variant.ident.clone()));
prefix_literals.push(lit);
}
let Some(doc_attr) = variant.attrs.iter().find(|a| a.path.is_ident("doc")) else {
return Err(Error::new(variant.span(), r#"expected a doc comment"#))
};
let variant_ident = variant.ident;
if variant_ident != "Ruff" {
let (name, url) = parse_doc_attr(doc_attr)?;
name_match_arms.extend(quote! {Self::#variant_ident => #name,});
url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),});
}
for lit in &prefixes {
parsed.push((lit.clone(), variant_ident.clone()));
}
prefix_match_arms.extend(quote! {
Self::#variant_ident => &[#(#prefix_literals),*],
Self::#variant_ident => &[#(#prefixes),*],
});
}
parsed.sort_by_key(|(prefix, _)| prefix.value().len());
parsed.sort_by_key(|(prefix, _)| prefix.len());
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();
@@ -55,7 +80,7 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
return Some((#ident::#field, rest));
}});
let prefix_ident = Ident::new(&prefix.value(), Span::call_site());
let prefix_ident = Ident::new(&prefix, Span::call_site());
if field != "Pycodestyle" {
into_iter_match_arms.extend(quote! {
@@ -82,6 +107,14 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
fn prefixes(&self) -> &'static [&'static str] {
match self { #prefix_match_arms }
}
fn name(&self) -> &'static str {
match self { #name_match_arms }
}
fn url(&self) -> Option<&'static str> {
match self { #url_match_arms }
}
}
impl IntoIterator for &#ident {
@@ -98,3 +131,23 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
}
})
}
/// Parses an attribute in the form of `#[doc = " [name](https://example.com/)"]`
/// into a tuple of link label and URL.
fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(doc_lit), ..})) = doc_attr.parse_meta() else {
return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of #[doc = "..."]"#))
};
parse_markdown_link(doc_lit.value().trim())
.map(|(name, url)| (name.to_string(), url.to_string()))
.ok_or_else(|| {
Error::new(
doc_lit.span(),
r#"expected doc comment to be in the form of `/// [name](https://example.com/)`"#,
)
})
}
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
}

View File

@@ -84,7 +84,8 @@ mod tests {
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == '#[prefix = "RUF"]':
elif line.strip() == '/// Ruff-specific rules':
fp.write(f"/// [{plugin}]({url})\n")
fp.write(f'{indent}#[prefix = "{prefix_code}"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")

View File

@@ -116,7 +116,7 @@ pub fn {rule_name_snake}(checker: &mut Checker) {{}}
if line.strip() == f"// {linter}":
indent = get_indent(line)
fp.write(f"{indent}{code} => rules::{linter}::rules::{name},")
fp.write(f"{indent}{code} => rules::{dir_name(linter)}::rules::{name},")
fp.write("\n")
has_written = True

40
src/ast/hashable.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::hash::Hash;
use rustpython_ast::Expr;
use crate::ast::comparable::ComparableExpr;
/// Wrapper around `Expr` that implements `Hash` and `PartialEq`.
pub struct HashableExpr<'a>(&'a Expr);
impl Hash for HashableExpr<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let comparable = ComparableExpr::from(self.0);
comparable.hash(state);
}
}
impl PartialEq<Self> for HashableExpr<'_> {
fn eq(&self, other: &Self) -> bool {
let comparable = ComparableExpr::from(self.0);
comparable == ComparableExpr::from(other.0)
}
}
impl Eq for HashableExpr<'_> {}
impl<'a> From<&'a Expr> for HashableExpr<'a> {
fn from(expr: &'a Expr) -> Self {
Self(expr)
}
}
impl<'a> HashableExpr<'a> {
pub(crate) fn from_expr(expr: &'a Expr) -> Self {
Self(expr)
}
pub(crate) fn as_expr(&self) -> &'a Expr {
self.0
}
}

View File

@@ -2,6 +2,7 @@ pub mod branch_detection;
pub mod cast;
pub mod comparable;
pub mod function_type;
pub mod hashable;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -36,8 +36,8 @@ use crate::rules::{
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -1386,6 +1386,7 @@ where
stmt,
test,
body,
orelse,
self.current_stmt_parent().map(Into::into),
);
}
@@ -1578,6 +1579,9 @@ where
if self.settings.rules.enabled(&Rule::TryConsiderElse) {
tryceratops::rules::try_consider_else(self, body, orelse);
}
if self.settings.rules.enabled(&Rule::VerboseRaise) {
tryceratops::rules::verbose_raise(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
@@ -2203,6 +2207,11 @@ where
flake8_bugbear::rules::zip_without_explicit_strict(self, expr, func, keywords);
}
// flake8-pie
if self.settings.rules.enabled(&Rule::NoUnnecessaryDictKwargs) {
flake8_pie::rules::no_unnecessary_dict_kwargs(self, expr, keywords);
}
// flake8-bandit
if self.settings.rules.enabled(&Rule::ExecUsed) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
@@ -2249,6 +2258,15 @@ where
if self.settings.rules.enabled(&Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(self, func, args, keywords);
}
if self
.settings
.rules
.enabled(&Rule::LoggingConfigInsecureListen)
{
flake8_bandit::rules::logging_config_insecure_listen(
self, func, args, keywords,
);
}
// flake8-comprehensions
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorList) {
@@ -2545,6 +2563,35 @@ where
{
flake8_simplify::rules::open_file_with_context_handler(self, func);
}
// flake8-use-pathlib
if self.settings.rules.enabled(&Rule::PathlibAbspath)
|| self.settings.rules.enabled(&Rule::PathlibChmod)
|| self.settings.rules.enabled(&Rule::PathlibMkdir)
|| self.settings.rules.enabled(&Rule::PathlibMakedirs)
|| self.settings.rules.enabled(&Rule::PathlibRename)
|| self.settings.rules.enabled(&Rule::PathlibReplace)
|| self.settings.rules.enabled(&Rule::PathlibRmdir)
|| self.settings.rules.enabled(&Rule::PathlibRemove)
|| self.settings.rules.enabled(&Rule::PathlibUnlink)
|| self.settings.rules.enabled(&Rule::PathlibGetcwd)
|| self.settings.rules.enabled(&Rule::PathlibExists)
|| self.settings.rules.enabled(&Rule::PathlibExpanduser)
|| self.settings.rules.enabled(&Rule::PathlibIsDir)
|| self.settings.rules.enabled(&Rule::PathlibIsFile)
|| self.settings.rules.enabled(&Rule::PathlibIsLink)
|| self.settings.rules.enabled(&Rule::PathlibReadlink)
|| self.settings.rules.enabled(&Rule::PathlibStat)
|| self.settings.rules.enabled(&Rule::PathlibIsAbs)
|| self.settings.rules.enabled(&Rule::PathlibJoin)
|| self.settings.rules.enabled(&Rule::PathlibBasename)
|| self.settings.rules.enabled(&Rule::PathlibSamefile)
|| self.settings.rules.enabled(&Rule::PathlibSplitext)
|| self.settings.rules.enabled(&Rule::PathlibOpen)
|| self.settings.rules.enabled(&Rule::PathlibPyPath)
{
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
}
ExprKind::Dict { keys, values } => {
if self
@@ -2558,6 +2605,10 @@ where
{
pyflakes::rules::repeated_keys(self, keys, values);
}
if self.settings.rules.enabled(&Rule::NoUnnecessarySpread) {
flake8_pie::rules::no_unnecessary_spread(self, keys, values);
}
}
ExprKind::Yield { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {

View File

@@ -338,6 +338,7 @@ ruff_macros::define_rule_mapping!(
S506 => violations::UnsafeYAMLLoad,
S508 => violations::SnmpInsecureVersion,
S509 => violations::SnmpWeakCryptography,
S612 => rules::flake8_bandit::rules::LoggingConfigInsecureListen,
S701 => violations::Jinja2AutoescapeFalse,
// flake8-boolean-trap
FBT001 => violations::BooleanPositionalArgInFunctionDefinition,
@@ -413,6 +414,8 @@ ruff_macros::define_rule_mapping!(
PIE790 => violations::NoUnnecessaryPass,
PIE794 => violations::DupeClassFieldDefinitions,
PIE796 => violations::PreferUniqueEnums,
PIE800 => violations::NoUnnecessarySpread,
PIE804 => violations::NoUnnecessaryDictKwargs,
PIE807 => violations::PreferListBuiltin,
// flake8-commas
COM812 => violations::TrailingCommaMissing,
@@ -428,7 +431,34 @@ ruff_macros::define_rule_mapping!(
TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops
TRY004 => rules::tryceratops::rules::PreferTypeError,
TRY201 => rules::tryceratops::rules::VerboseRaise,
TRY300 => rules::tryceratops::rules::TryConsiderElse,
// flake8-use-pathlib
PTH100 => rules::flake8_use_pathlib::violations::PathlibAbspath,
PTH101 => rules::flake8_use_pathlib::violations::PathlibChmod,
PTH102 => rules::flake8_use_pathlib::violations::PathlibMkdir,
PTH103 => rules::flake8_use_pathlib::violations::PathlibMakedirs,
PTH104 => rules::flake8_use_pathlib::violations::PathlibRename,
PTH105 => rules::flake8_use_pathlib::violations::PathlibReplace,
PTH106 => rules::flake8_use_pathlib::violations::PathlibRmdir,
PTH107 => rules::flake8_use_pathlib::violations::PathlibRemove,
PTH108 => rules::flake8_use_pathlib::violations::PathlibUnlink,
PTH109 => rules::flake8_use_pathlib::violations::PathlibGetcwd,
PTH110 => rules::flake8_use_pathlib::violations::PathlibExists,
PTH111 => rules::flake8_use_pathlib::violations::PathlibExpanduser,
PTH112 => rules::flake8_use_pathlib::violations::PathlibIsDir,
PTH113 => rules::flake8_use_pathlib::violations::PathlibIsFile,
PTH114 => rules::flake8_use_pathlib::violations::PathlibIsLink,
PTH115 => rules::flake8_use_pathlib::violations::PathlibReadlink,
PTH116 => rules::flake8_use_pathlib::violations::PathlibStat,
PTH117 => rules::flake8_use_pathlib::violations::PathlibIsAbs,
PTH118 => rules::flake8_use_pathlib::violations::PathlibJoin,
PTH119 => rules::flake8_use_pathlib::violations::PathlibBasename,
PTH120 => rules::flake8_use_pathlib::violations::PathlibDirname,
PTH121 => rules::flake8_use_pathlib::violations::PathlibSamefile,
PTH122 => rules::flake8_use_pathlib::violations::PathlibSplitext,
PTH123 => rules::flake8_use_pathlib::violations::PathlibOpen,
PTH124 => rules::flake8_use_pathlib::violations::PathlibPyPath,
// ruff
RUF001 => violations::AmbiguousUnicodeCharacterString,
RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
@@ -440,81 +470,122 @@ ruff_macros::define_rule_mapping!(
#[derive(EnumIter, Debug, PartialEq, Eq, RuleNamespace)]
pub enum Linter {
/// [Pyflakes](https://pypi.org/project/pyflakes/)
#[prefix = "F"]
Pyflakes,
/// [pycodestyle](https://pypi.org/project/pycodestyle/)
#[prefix = "E"]
#[prefix = "W"]
Pycodestyle,
/// [mccabe](https://pypi.org/project/mccabe/)
#[prefix = "C90"]
McCabe,
/// [isort](https://pypi.org/project/isort/)
#[prefix = "I"]
Isort,
/// [pydocstyle](https://pypi.org/project/pydocstyle/)
#[prefix = "D"]
Pydocstyle,
/// [pyupgrade](https://pypi.org/project/pyupgrade/)
#[prefix = "UP"]
Pyupgrade,
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
#[prefix = "N"]
PEP8Naming,
/// [flake8-2020](https://pypi.org/project/flake8-2020/)
#[prefix = "YTT"]
Flake82020,
/// [flake8-annotations](https://pypi.org/project/flake8-annotations/)
#[prefix = "ANN"]
Flake8Annotations,
/// [flake8-bandit](https://pypi.org/project/flake8-bandit/)
#[prefix = "S"]
Flake8Bandit,
/// [flake8-blind-except](https://pypi.org/project/flake8-blind-except/)
#[prefix = "BLE"]
Flake8BlindExcept,
/// [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/)
#[prefix = "FBT"]
Flake8BooleanTrap,
/// [flake8-bugbear](https://pypi.org/project/flake8-bugbear/)
#[prefix = "B"]
Flake8Bugbear,
/// [flake8-builtins](https://pypi.org/project/flake8-builtins/)
#[prefix = "A"]
Flake8Builtins,
/// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
#[prefix = "C4"]
Flake8Comprehensions,
/// [flake8-debugger](https://pypi.org/project/flake8-debugger/)
#[prefix = "T10"]
Flake8Debugger,
/// [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
#[prefix = "EM"]
Flake8ErrMsg,
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
#[prefix = "ISC"]
Flake8ImplicitStrConcat,
/// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
#[prefix = "ICN"]
Flake8ImportConventions,
/// [flake8-print](https://pypi.org/project/flake8-print/)
#[prefix = "T20"]
Flake8Print,
/// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
#[prefix = "PT"]
Flake8PytestStyle,
/// [flake8-quotes](https://pypi.org/project/flake8-quotes/)
#[prefix = "Q"]
Flake8Quotes,
/// [flake8-return](https://pypi.org/project/flake8-return/)
#[prefix = "RET"]
Flake8Return,
/// [flake8-simplify](https://pypi.org/project/flake8-simplify/)
#[prefix = "SIM"]
Flake8Simplify,
/// [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
#[prefix = "TID"]
Flake8TidyImports,
/// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/)
#[prefix = "ARG"]
Flake8UnusedArguments,
/// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
#[prefix = "DTZ"]
Flake8Datetimez,
/// [eradicate](https://pypi.org/project/eradicate/)
#[prefix = "ERA"]
Eradicate,
/// [pandas-vet](https://pypi.org/project/pandas-vet/)
#[prefix = "PD"]
PandasVet,
/// [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks)
#[prefix = "PGH"]
PygrepHooks,
/// [Pylint](https://pypi.org/project/pylint/)
#[prefix = "PL"]
Pylint,
/// [flake8-pie](https://pypi.org/project/flake8-pie/)
#[prefix = "PIE"]
Flake8Pie,
/// [flake8-commas](https://pypi.org/project/flake8-commas/)
#[prefix = "COM"]
Flake8Commas,
/// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/)
#[prefix = "INP"]
Flake8NoPep420,
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
#[prefix = "EXE"]
Flake8Executable,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TYP"]
Flake8TypeChecking,
/// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
#[prefix = "TRY"]
Tryceratops,
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
#[prefix = "PTH"]
Flake8UsePathlib,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
}
@@ -523,9 +594,11 @@ pub trait RuleNamespace: Sized {
fn parse_code(code: &str) -> Option<(Self, &str)>;
fn prefixes(&self) -> &'static [&'static str];
}
include!(concat!(env!("OUT_DIR"), "/linter.rs"));
fn name(&self) -> &'static str;
fn url(&self) -> Option<&'static str>;
}
/// The prefix, name and selector for an upstream linter category.
pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleSelector);

View File

@@ -28,6 +28,7 @@ mod tests {
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"); "S508")]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"); "S509")]
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());

View File

@@ -0,0 +1,40 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, Keyword};
use crate::ast::helpers::SimpleCallArgs;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct LoggingConfigInsecureListen;
);
impl Violation for LoggingConfigInsecureListen {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure `logging.config.listen` detected")
}
}
/// S612
pub fn logging_config_insecure_listen(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker.resolve_call_path(func).map_or(false, |call_path| {
call_path.as_slice() == ["logging", "config", "listen"]
}) {
let call_args = SimpleCallArgs::new(args, keywords);
if call_args.get_argument("verify", None).is_none() {
checker.diagnostics.push(Diagnostic::new(
LoggingConfigInsecureListen,
Range::from_located(func),
));
}
}
}

View File

@@ -10,6 +10,9 @@ pub use hardcoded_password_string::{
pub use hardcoded_tmp_directory::hardcoded_tmp_directory;
pub use hashlib_insecure_hash_functions::hashlib_insecure_hash_functions;
pub use jinja2_autoescape_false::jinja2_autoescape_false;
pub use logging_config_insecure_listen::{
logging_config_insecure_listen, LoggingConfigInsecureListen,
};
pub use request_with_no_cert_validation::request_with_no_cert_validation;
pub use request_without_timeout::request_without_timeout;
pub use snmp_insecure_version::snmp_insecure_version;
@@ -26,6 +29,7 @@ mod hardcoded_password_string;
mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod jinja2_autoescape_false;
mod logging_config_insecure_listen;
mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
LoggingConfigInsecureListen: ~
location:
row: 3
column: 4
end_location:
row: 3
column: 25
fix: ~
parent: ~

View File

@@ -1,5 +1,6 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
@@ -20,7 +21,7 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E
continue;
};
let mut diagnostic = Diagnostic::new(
violations::RedundantTupleInExceptionHandler(elt.to_string()),
violations::RedundantTupleInExceptionHandler(unparse_expr(elt, checker.stylist)),
Range::from_located(type_),
);
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -12,10 +12,12 @@ mod tests {
use crate::registry::Rule;
use crate::settings;
#[test_case(Rule::NoUnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::DupeClassFieldDefinitions, Path::new("PIE794.py"); "PIE794")]
#[test_case(Rule::PreferUniqueEnums, Path::new("PIE796.py"); "PIE796")]
#[test_case(Rule::NoUnnecessaryDictKwargs, Path::new("PIE804.py"); "PIE804")]
#[test_case(Rule::NoUnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::NoUnnecessarySpread, Path::new("PIE800.py"); "PIE800")]
#[test_case(Rule::PreferListBuiltin, Path::new("PIE807.py"); "PIE807")]
#[test_case(Rule::PreferUniqueEnums, Path::new("PIE796.py"); "PIE796")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,6 +1,6 @@
use log::error;
use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::comparable::ComparableExpr;
use crate::ast::helpers::unparse_expr;
@@ -8,6 +8,7 @@ use crate::ast::types::{Range, RefEquality};
use crate::autofix::helpers::delete_stmt;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::registry::{Diagnostic, Rule};
use crate::violations;
@@ -152,6 +153,56 @@ where
}
}
/// PIE800
pub fn no_unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
for item in keys.iter().zip(values.iter()) {
if let (None, value) = item {
// We only care about when the key is None which indicates a spread `**`
// inside a dict.
if let ExprKind::Dict { .. } = value.node {
let diagnostic =
Diagnostic::new(violations::NoUnnecessarySpread, Range::from_located(value));
checker.diagnostics.push(diagnostic);
}
}
}
}
/// Return `true` if a key is a valid keyword argument name.
fn is_valid_kwarg_name(key: &Expr) -> bool {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &key.node
{
is_identifier(value)
} else {
false
}
}
/// PIE804
pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
for kw in kwargs {
// keyword is a spread operator (indicated by None)
if kw.node.arg.is_none() {
if let ExprKind::Dict { keys, .. } = &kw.node.value.node {
// ensure foo(**{"bar-bar": 1}) doesn't error
if keys.iter().all(|expr| expr.as_ref().map_or(false, is_valid_kwarg_name)) ||
// handle case of foo(**{**bar})
(keys.len() == 1 && keys[0].is_none())
{
let diagnostic = Diagnostic::new(
violations::NoUnnecessaryDictKwargs,
Range::from_located(expr),
);
checker.diagnostics.push(diagnostic);
}
}
}
}
}
/// PIE807
pub fn prefer_list_builtin(checker: &mut Checker, expr: &Expr) {
let ExprKind::Lambda { args, body } = &expr.node else {

View File

@@ -0,0 +1,45 @@
---
source: src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessarySpread: ~
location:
row: 1
column: 13
end_location:
row: 1
column: 23
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 3
column: 14
end_location:
row: 3
column: 27
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 5
column: 10
end_location:
row: 5
column: 21
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 7
column: 18
end_location:
row: 7
column: 27
fix: ~
parent: ~

View File

@@ -0,0 +1,55 @@
---
source: src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 1
column: 0
end_location:
row: 1
column: 20
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 3
column: 0
end_location:
row: 3
column: 21
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 5
column: 0
end_location:
row: 5
column: 35
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 7
column: 0
end_location:
row: 7
column: 38
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 9
column: 0
end_location:
row: 9
column: 29
fix: ~
parent: ~

View File

@@ -50,7 +50,7 @@ fn is_main_check(expr: &Expr) -> bool {
/// ```
fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> {
let [Stmt { node: StmtKind::If { test, body: inner_body, orelse }, ..}] = body else { return None };
if !(orelse.is_empty() && body.len() == 1) {
if !orelse.is_empty() {
return None;
}
find_last_nested_if(inner_body).or_else(|| {
@@ -67,12 +67,10 @@ pub fn nested_if_statements(
stmt: &Stmt,
test: &Expr,
body: &[Stmt],
orelse: &[Stmt],
parent: Option<&Stmt>,
) {
if is_main_check(test) {
return;
}
// If the parent could contain a nested if-statement, abort.
if let Some(parent) = parent {
if let StmtKind::If { body, orelse, .. } = &parent.node {
if orelse.is_empty() && body.len() == 1 {
@@ -81,6 +79,16 @@ pub fn nested_if_statements(
}
}
// If this if-statement has an else clause, or more than one child, abort.
if !(orelse.is_empty() && body.len() == 1) {
return;
}
if is_main_check(test) {
return;
}
// Find the deepest nested if-statement, to inform the range.
let Some((test, first_stmt)) = find_last_nested_if(body) else {
return;
};

View File

@@ -148,4 +148,21 @@ expression: diagnostics
row: 95
column: 0
parent: ~
- kind:
NestedIfStatements: ~
location:
row: 117
column: 4
end_location:
row: 118
column: 13
fix:
content: " if b and c:\n print(\"foo\")\n"
location:
row: 117
column: 0
end_location:
row: 120
column: 0
parent: ~

View File

@@ -0,0 +1,54 @@
use rustpython_ast::Expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, DiagnosticKind};
use crate::rules::flake8_use_pathlib::violations::{
PathlibAbspath, PathlibBasename, PathlibChmod, PathlibDirname, PathlibExists,
PathlibExpanduser, PathlibGetcwd, PathlibIsAbs, PathlibIsDir, PathlibIsFile, PathlibIsLink,
PathlibJoin, PathlibMakedirs, PathlibMkdir, PathlibOpen, PathlibPyPath, PathlibReadlink,
PathlibRemove, PathlibRename, PathlibReplace, PathlibRmdir, PathlibSamefile, PathlibSplitext,
PathlibStat, PathlibUnlink,
};
pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
if let Some(diagnostic_kind) =
checker
.resolve_call_path(expr)
.and_then(|call_path| match call_path.as_slice() {
["os", "path", "abspath"] => Some(PathlibAbspath.into()),
["os", "chmod"] => Some(PathlibChmod.into()),
["os", "mkdir"] => Some(PathlibMkdir.into()),
["os", "makedirs"] => Some(PathlibMakedirs.into()),
["os", "rename"] => Some(PathlibRename.into()),
["os", "replace"] => Some(PathlibReplace.into()),
["os", "rmdir"] => Some(PathlibRmdir.into()),
["os", "remove"] => Some(PathlibRemove.into()),
["os", "unlink"] => Some(PathlibUnlink.into()),
["os", "getcwd"] => Some(PathlibGetcwd.into()),
["os", "path", "exists"] => Some(PathlibExists.into()),
["os", "path", "expanduser"] => Some(PathlibExpanduser.into()),
["os", "path", "isdir"] => Some(PathlibIsDir.into()),
["os", "path", "isfile"] => Some(PathlibIsFile.into()),
["os", "path", "islink"] => Some(PathlibIsLink.into()),
["os", "readlink"] => Some(PathlibReadlink.into()),
["os", "stat"] => Some(PathlibStat.into()),
["os", "path", "isabs"] => Some(PathlibIsAbs.into()),
["os", "path", "join"] => Some(PathlibJoin.into()),
["os", "path", "basename"] => Some(PathlibBasename.into()),
["os", "path", "dirname"] => Some(PathlibDirname.into()),
["os", "path", "samefile"] => Some(PathlibSamefile.into()),
["os", "path", "splitext"] => Some(PathlibSplitext.into()),
["", "open"] => Some(PathlibOpen.into()),
["py", "path", "local"] => Some(PathlibPyPath.into()),
_ => None,
})
{
let diagnostic =
Diagnostic::new::<DiagnosticKind>(diagnostic_kind, Range::from_located(expr));
if checker.settings.rules.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -0,0 +1,70 @@
//! Rules from [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/).
pub(crate) mod helpers;
pub(crate) mod violations;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::settings;
#[test_case(Path::new("full_name.py"); "PTH1_1")]
#[test_case(Path::new("import_as.py"); "PTH1_2")]
#[test_case(Path::new("import_from_as.py"); "PTH1_3")]
#[test_case(Path::new("import_from.py"); "PTH1_4")]
fn rules(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_use_pathlib")
.join(path)
.as_path(),
&settings::Settings::for_rules(vec![
Rule::PathlibAbspath,
Rule::PathlibChmod,
Rule::PathlibMkdir,
Rule::PathlibMakedirs,
Rule::PathlibRename,
Rule::PathlibReplace,
Rule::PathlibRmdir,
Rule::PathlibRemove,
Rule::PathlibUnlink,
Rule::PathlibGetcwd,
Rule::PathlibExists,
Rule::PathlibExpanduser,
Rule::PathlibIsDir,
Rule::PathlibIsFile,
Rule::PathlibIsLink,
Rule::PathlibReadlink,
Rule::PathlibStat,
Rule::PathlibIsAbs,
Rule::PathlibJoin,
Rule::PathlibBasename,
Rule::PathlibDirname,
Rule::PathlibSamefile,
Rule::PathlibSplitext,
Rule::PathlibOpen,
]),
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::PathlibPyPath, Path::new("py_path_1.py"); "PTH024_1")]
#[test_case(Rule::PathlibPyPath, Path::new("py_path_2.py"); "PTH024_2")]
fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_use_pathlib")
.join(path)
.as_path(),
&settings::Settings::for_rule(rule_code),
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibPyPath: ~
location:
row: 3
column: 4
end_location:
row: 3
column: 17
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibPyPath: ~
location:
row: 3
column: 4
end_location:
row: 3
column: 8
fix: ~
parent: ~

View File

@@ -0,0 +1,255 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibAbspath: ~
location:
row: 6
column: 4
end_location:
row: 6
column: 19
fix: ~
parent: ~
- kind:
PathlibChmod: ~
location:
row: 7
column: 5
end_location:
row: 7
column: 13
fix: ~
parent: ~
- kind:
PathlibMkdir: ~
location:
row: 8
column: 6
end_location:
row: 8
column: 14
fix: ~
parent: ~
- kind:
PathlibMakedirs: ~
location:
row: 9
column: 0
end_location:
row: 9
column: 11
fix: ~
parent: ~
- kind:
PathlibRename: ~
location:
row: 10
column: 0
end_location:
row: 10
column: 9
fix: ~
parent: ~
- kind:
PathlibReplace: ~
location:
row: 11
column: 0
end_location:
row: 11
column: 10
fix: ~
parent: ~
- kind:
PathlibRmdir: ~
location:
row: 12
column: 0
end_location:
row: 12
column: 8
fix: ~
parent: ~
- kind:
PathlibRemove: ~
location:
row: 13
column: 0
end_location:
row: 13
column: 9
fix: ~
parent: ~
- kind:
PathlibUnlink: ~
location:
row: 14
column: 0
end_location:
row: 14
column: 9
fix: ~
parent: ~
- kind:
PathlibGetcwd: ~
location:
row: 15
column: 0
end_location:
row: 15
column: 9
fix: ~
parent: ~
- kind:
PathlibExists: ~
location:
row: 16
column: 4
end_location:
row: 16
column: 18
fix: ~
parent: ~
- kind:
PathlibExpanduser: ~
location:
row: 17
column: 5
end_location:
row: 17
column: 23
fix: ~
parent: ~
- kind:
PathlibIsDir: ~
location:
row: 18
column: 6
end_location:
row: 18
column: 19
fix: ~
parent: ~
- kind:
PathlibIsFile: ~
location:
row: 19
column: 7
end_location:
row: 19
column: 21
fix: ~
parent: ~
- kind:
PathlibIsLink: ~
location:
row: 20
column: 8
end_location:
row: 20
column: 22
fix: ~
parent: ~
- kind:
PathlibReadlink: ~
location:
row: 21
column: 0
end_location:
row: 21
column: 11
fix: ~
parent: ~
- kind:
PathlibStat: ~
location:
row: 22
column: 0
end_location:
row: 22
column: 7
fix: ~
parent: ~
- kind:
PathlibIsAbs: ~
location:
row: 23
column: 0
end_location:
row: 23
column: 13
fix: ~
parent: ~
- kind:
PathlibJoin: ~
location:
row: 24
column: 0
end_location:
row: 24
column: 12
fix: ~
parent: ~
- kind:
PathlibBasename: ~
location:
row: 25
column: 0
end_location:
row: 25
column: 16
fix: ~
parent: ~
- kind:
PathlibDirname: ~
location:
row: 26
column: 0
end_location:
row: 26
column: 15
fix: ~
parent: ~
- kind:
PathlibSamefile: ~
location:
row: 27
column: 0
end_location:
row: 27
column: 16
fix: ~
parent: ~
- kind:
PathlibSplitext: ~
location:
row: 28
column: 0
end_location:
row: 28
column: 16
fix: ~
parent: ~
- kind:
PathlibOpen: ~
location:
row: 29
column: 5
end_location:
row: 29
column: 9
fix: ~
parent: ~
- kind:
PathlibOpen: ~
location:
row: 31
column: 0
end_location:
row: 31
column: 4
fix: ~
parent: ~

View File

@@ -0,0 +1,235 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibAbspath: ~
location:
row: 6
column: 4
end_location:
row: 6
column: 17
fix: ~
parent: ~
- kind:
PathlibChmod: ~
location:
row: 7
column: 5
end_location:
row: 7
column: 14
fix: ~
parent: ~
- kind:
PathlibMkdir: ~
location:
row: 8
column: 6
end_location:
row: 8
column: 15
fix: ~
parent: ~
- kind:
PathlibMakedirs: ~
location:
row: 9
column: 0
end_location:
row: 9
column: 12
fix: ~
parent: ~
- kind:
PathlibRename: ~
location:
row: 10
column: 0
end_location:
row: 10
column: 10
fix: ~
parent: ~
- kind:
PathlibReplace: ~
location:
row: 11
column: 0
end_location:
row: 11
column: 11
fix: ~
parent: ~
- kind:
PathlibRmdir: ~
location:
row: 12
column: 0
end_location:
row: 12
column: 9
fix: ~
parent: ~
- kind:
PathlibRemove: ~
location:
row: 13
column: 0
end_location:
row: 13
column: 10
fix: ~
parent: ~
- kind:
PathlibUnlink: ~
location:
row: 14
column: 0
end_location:
row: 14
column: 10
fix: ~
parent: ~
- kind:
PathlibGetcwd: ~
location:
row: 15
column: 0
end_location:
row: 15
column: 10
fix: ~
parent: ~
- kind:
PathlibExists: ~
location:
row: 16
column: 4
end_location:
row: 16
column: 16
fix: ~
parent: ~
- kind:
PathlibExpanduser: ~
location:
row: 17
column: 5
end_location:
row: 17
column: 21
fix: ~
parent: ~
- kind:
PathlibIsDir: ~
location:
row: 18
column: 6
end_location:
row: 18
column: 17
fix: ~
parent: ~
- kind:
PathlibIsFile: ~
location:
row: 19
column: 7
end_location:
row: 19
column: 19
fix: ~
parent: ~
- kind:
PathlibIsLink: ~
location:
row: 20
column: 8
end_location:
row: 20
column: 20
fix: ~
parent: ~
- kind:
PathlibReadlink: ~
location:
row: 21
column: 0
end_location:
row: 21
column: 12
fix: ~
parent: ~
- kind:
PathlibStat: ~
location:
row: 22
column: 0
end_location:
row: 22
column: 8
fix: ~
parent: ~
- kind:
PathlibIsAbs: ~
location:
row: 23
column: 0
end_location:
row: 23
column: 11
fix: ~
parent: ~
- kind:
PathlibJoin: ~
location:
row: 24
column: 0
end_location:
row: 24
column: 10
fix: ~
parent: ~
- kind:
PathlibBasename: ~
location:
row: 25
column: 0
end_location:
row: 25
column: 14
fix: ~
parent: ~
- kind:
PathlibDirname: ~
location:
row: 26
column: 0
end_location:
row: 26
column: 13
fix: ~
parent: ~
- kind:
PathlibSamefile: ~
location:
row: 27
column: 0
end_location:
row: 27
column: 14
fix: ~
parent: ~
- kind:
PathlibSplitext: ~
location:
row: 28
column: 0
end_location:
row: 28
column: 14
fix: ~
parent: ~

View File

@@ -0,0 +1,255 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibAbspath: ~
location:
row: 8
column: 4
end_location:
row: 8
column: 11
fix: ~
parent: ~
- kind:
PathlibChmod: ~
location:
row: 9
column: 5
end_location:
row: 9
column: 10
fix: ~
parent: ~
- kind:
PathlibMkdir: ~
location:
row: 10
column: 6
end_location:
row: 10
column: 11
fix: ~
parent: ~
- kind:
PathlibMakedirs: ~
location:
row: 11
column: 0
end_location:
row: 11
column: 8
fix: ~
parent: ~
- kind:
PathlibRename: ~
location:
row: 12
column: 0
end_location:
row: 12
column: 6
fix: ~
parent: ~
- kind:
PathlibReplace: ~
location:
row: 13
column: 0
end_location:
row: 13
column: 7
fix: ~
parent: ~
- kind:
PathlibRmdir: ~
location:
row: 14
column: 0
end_location:
row: 14
column: 5
fix: ~
parent: ~
- kind:
PathlibRemove: ~
location:
row: 15
column: 0
end_location:
row: 15
column: 6
fix: ~
parent: ~
- kind:
PathlibUnlink: ~
location:
row: 16
column: 0
end_location:
row: 16
column: 6
fix: ~
parent: ~
- kind:
PathlibGetcwd: ~
location:
row: 17
column: 0
end_location:
row: 17
column: 6
fix: ~
parent: ~
- kind:
PathlibExists: ~
location:
row: 18
column: 4
end_location:
row: 18
column: 10
fix: ~
parent: ~
- kind:
PathlibExpanduser: ~
location:
row: 19
column: 5
end_location:
row: 19
column: 15
fix: ~
parent: ~
- kind:
PathlibIsDir: ~
location:
row: 20
column: 6
end_location:
row: 20
column: 11
fix: ~
parent: ~
- kind:
PathlibIsFile: ~
location:
row: 21
column: 7
end_location:
row: 21
column: 13
fix: ~
parent: ~
- kind:
PathlibIsLink: ~
location:
row: 22
column: 8
end_location:
row: 22
column: 14
fix: ~
parent: ~
- kind:
PathlibReadlink: ~
location:
row: 23
column: 0
end_location:
row: 23
column: 8
fix: ~
parent: ~
- kind:
PathlibStat: ~
location:
row: 24
column: 0
end_location:
row: 24
column: 4
fix: ~
parent: ~
- kind:
PathlibIsAbs: ~
location:
row: 25
column: 0
end_location:
row: 25
column: 5
fix: ~
parent: ~
- kind:
PathlibJoin: ~
location:
row: 26
column: 0
end_location:
row: 26
column: 4
fix: ~
parent: ~
- kind:
PathlibBasename: ~
location:
row: 27
column: 0
end_location:
row: 27
column: 8
fix: ~
parent: ~
- kind:
PathlibDirname: ~
location:
row: 28
column: 0
end_location:
row: 28
column: 7
fix: ~
parent: ~
- kind:
PathlibSamefile: ~
location:
row: 29
column: 0
end_location:
row: 29
column: 8
fix: ~
parent: ~
- kind:
PathlibSplitext: ~
location:
row: 30
column: 0
end_location:
row: 30
column: 8
fix: ~
parent: ~
- kind:
PathlibOpen: ~
location:
row: 31
column: 5
end_location:
row: 31
column: 9
fix: ~
parent: ~
- kind:
PathlibOpen: ~
location:
row: 33
column: 0
end_location:
row: 33
column: 4
fix: ~
parent: ~

View File

@@ -0,0 +1,235 @@
---
source: src/rules/flake8_use_pathlib/mod.rs
expression: diagnostics
---
- kind:
PathlibAbspath: ~
location:
row: 13
column: 4
end_location:
row: 13
column: 12
fix: ~
parent: ~
- kind:
PathlibChmod: ~
location:
row: 14
column: 5
end_location:
row: 14
column: 11
fix: ~
parent: ~
- kind:
PathlibMkdir: ~
location:
row: 15
column: 6
end_location:
row: 15
column: 12
fix: ~
parent: ~
- kind:
PathlibMakedirs: ~
location:
row: 16
column: 0
end_location:
row: 16
column: 9
fix: ~
parent: ~
- kind:
PathlibRename: ~
location:
row: 17
column: 0
end_location:
row: 17
column: 7
fix: ~
parent: ~
- kind:
PathlibReplace: ~
location:
row: 18
column: 0
end_location:
row: 18
column: 8
fix: ~
parent: ~
- kind:
PathlibRmdir: ~
location:
row: 19
column: 0
end_location:
row: 19
column: 6
fix: ~
parent: ~
- kind:
PathlibRemove: ~
location:
row: 20
column: 0
end_location:
row: 20
column: 7
fix: ~
parent: ~
- kind:
PathlibUnlink: ~
location:
row: 21
column: 0
end_location:
row: 21
column: 7
fix: ~
parent: ~
- kind:
PathlibGetcwd: ~
location:
row: 22
column: 0
end_location:
row: 22
column: 7
fix: ~
parent: ~
- kind:
PathlibExists: ~
location:
row: 23
column: 4
end_location:
row: 23
column: 11
fix: ~
parent: ~
- kind:
PathlibExpanduser: ~
location:
row: 24
column: 5
end_location:
row: 24
column: 16
fix: ~
parent: ~
- kind:
PathlibIsDir: ~
location:
row: 25
column: 6
end_location:
row: 25
column: 12
fix: ~
parent: ~
- kind:
PathlibIsFile: ~
location:
row: 26
column: 7
end_location:
row: 26
column: 14
fix: ~
parent: ~
- kind:
PathlibIsLink: ~
location:
row: 27
column: 8
end_location:
row: 27
column: 15
fix: ~
parent: ~
- kind:
PathlibReadlink: ~
location:
row: 28
column: 0
end_location:
row: 28
column: 9
fix: ~
parent: ~
- kind:
PathlibStat: ~
location:
row: 29
column: 0
end_location:
row: 29
column: 5
fix: ~
parent: ~
- kind:
PathlibIsAbs: ~
location:
row: 30
column: 0
end_location:
row: 30
column: 6
fix: ~
parent: ~
- kind:
PathlibJoin: ~
location:
row: 31
column: 0
end_location:
row: 31
column: 5
fix: ~
parent: ~
- kind:
PathlibBasename: ~
location:
row: 32
column: 0
end_location:
row: 32
column: 9
fix: ~
parent: ~
- kind:
PathlibDirname: ~
location:
row: 33
column: 0
end_location:
row: 33
column: 8
fix: ~
parent: ~
- kind:
PathlibSamefile: ~
location:
row: 34
column: 0
end_location:
row: 34
column: 9
fix: ~
parent: ~
- kind:
PathlibSplitext: ~
location:
row: 35
column: 0
end_location:
row: 35
column: 9
fix: ~
parent: ~

View File

@@ -0,0 +1,279 @@
use ruff_macros::derive_message_formats;
use crate::define_violation;
use crate::violation::Violation;
// PTH100
define_violation!(
pub struct PathlibAbspath;
);
impl Violation for PathlibAbspath {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.abspath` should be replaced by `.resolve()`")
}
}
// PTH101
define_violation!(
pub struct PathlibChmod;
);
impl Violation for PathlibChmod {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.chmod` should be replaced by `.chmod()`")
}
}
// PTH102
define_violation!(
pub struct PathlibMakedirs;
);
impl Violation for PathlibMakedirs {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.makedirs` should be replaced by `.mkdir(parents=True)`")
}
}
// PTH103
define_violation!(
pub struct PathlibMkdir;
);
impl Violation for PathlibMkdir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.mkdir` should be replaced by `.mkdir()`")
}
}
// PTH104
define_violation!(
pub struct PathlibRename;
);
impl Violation for PathlibRename {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.rename` should be replaced by `.rename()`")
}
}
// PTH105
define_violation!(
pub struct PathlibReplace;
);
impl Violation for PathlibReplace {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.replace`should be replaced by `.replace()`")
}
}
// PTH106
define_violation!(
pub struct PathlibRmdir;
);
impl Violation for PathlibRmdir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.rmdir` should be replaced by `.rmdir()`")
}
}
// PTH107
define_violation!(
pub struct PathlibRemove;
);
impl Violation for PathlibRemove {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.remove` should be replaced by `.unlink()`")
}
}
// PTH108
define_violation!(
pub struct PathlibUnlink;
);
impl Violation for PathlibUnlink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.unlink` should be replaced by `.unlink()`")
}
}
// PTH109
define_violation!(
pub struct PathlibGetcwd;
);
impl Violation for PathlibGetcwd {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.getcwd()` should be replaced by `Path.cwd()`")
}
}
// PTH110
define_violation!(
pub struct PathlibExists;
);
impl Violation for PathlibExists {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.exists` should be replaced by `.exists()`")
}
}
// PTH111
define_violation!(
pub struct PathlibExpanduser;
);
impl Violation for PathlibExpanduser {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.expanduser` should be replaced by `.expanduser()`")
}
}
// PTH112
define_violation!(
pub struct PathlibIsDir;
);
impl Violation for PathlibIsDir {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isdir` should be replaced by `.is_dir()`")
}
}
// PTH113
define_violation!(
pub struct PathlibIsFile;
);
impl Violation for PathlibIsFile {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isfile` should be replaced by `.is_file()`")
}
}
// PTH114
define_violation!(
pub struct PathlibIsLink;
);
impl Violation for PathlibIsLink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.islink` should be replaced by `.is_symlink()`")
}
}
// PTH115
define_violation!(
pub struct PathlibReadlink;
);
impl Violation for PathlibReadlink {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.readlink(` should be replaced by `.readlink()`")
}
}
// PTH116
define_violation!(
pub struct PathlibStat;
);
impl Violation for PathlibStat {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.stat` should be replaced by `.stat()` or `.owner()` or `.group()`")
}
}
// PTH117
define_violation!(
pub struct PathlibIsAbs;
);
impl Violation for PathlibIsAbs {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.isabs` should be replaced by `.is_absolute()`")
}
}
// PTH118
define_violation!(
pub struct PathlibJoin;
);
impl Violation for PathlibJoin {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.join` should be replaced by foo_path / \"bar\"")
}
}
// PTH119
define_violation!(
pub struct PathlibBasename;
);
impl Violation for PathlibBasename {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.basename` should be replaced by `.name`")
}
}
// PTH120
define_violation!(
pub struct PathlibDirname;
);
impl Violation for PathlibDirname {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.dirname` should be replaced by `.parent`")
}
}
// PTH121
define_violation!(
pub struct PathlibSamefile;
);
impl Violation for PathlibSamefile {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.samefile` should be replaced by `.samefile()`")
}
}
// PTH122
define_violation!(
pub struct PathlibSplitext;
);
impl Violation for PathlibSplitext {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.splitext` should be replaced by `.suffix`")
}
}
// PTH123
define_violation!(
pub struct PathlibOpen;
);
impl Violation for PathlibOpen {
#[derive_message_formats]
fn message(&self) -> String {
format!("`open(\"foo\")` should be replaced by`Path(\"foo\").open()`")
}
}
// PTH124
define_violation!(
pub struct PathlibPyPath;
);
impl Violation for PathlibPyPath {
#[derive_message_formats]
fn message(&self) -> String {
format!("`py.path` is in maintenance mode, use `pathlib` instead")
}
}

View File

@@ -25,6 +25,7 @@ pub mod flake8_simplify;
pub mod flake8_tidy_imports;
pub mod flake8_type_checking;
pub mod flake8_unused_arguments;
pub mod flake8_use_pathlib;
pub mod isort;
pub mod mccabe;
pub mod pandas_vet;

View File

@@ -2,18 +2,31 @@ use imperative::Mood;
use once_cell::sync::Lazy;
use ruff_macros::derive_message_formats;
use crate::ast::cast;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::Docstring;
use crate::docstrings::definition::{DefinitionKind, Docstring};
use crate::registry::Diagnostic;
use crate::rules::pydocstyle::helpers::normalize_word;
use crate::violation::Violation;
use crate::visibility::{is_property, is_test};
static MOOD: Lazy<Mood> = Lazy::new(Mood::new);
/// D401
pub fn non_imperative_mood(checker: &mut Checker, docstring: &Docstring) {
let (
DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent)
| DefinitionKind::Method(parent)
) = &docstring.kind else {
return;
};
if is_test(cast::name(parent)) || is_property(checker, cast::decorator_list(parent)) {
return;
}
let body = docstring.body;
// Find first line, disregarding whitespace.

View File

@@ -5,51 +5,71 @@ expression: diagnostics
- kind:
NonImperativeMood: Returns foo.
location:
row: 4
row: 8
column: 4
end_location:
row: 4
row: 8
column: 22
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a foo.
location:
row: 8
row: 12
column: 4
end_location:
row: 8
row: 12
column: 32
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a boa.
location:
row: 12
row: 16
column: 4
end_location:
row: 16
row: 20
column: 7
fix: ~
parent: ~
- kind:
NonImperativeMood: Runs something
location:
row: 20
row: 24
column: 4
end_location:
row: 20
row: 24
column: 24
fix: ~
parent: ~
- kind:
NonImperativeMood: "Runs other things, nested"
location:
row: 27
column: 8
end_location:
row: 27
column: 39
fix: ~
parent: ~
- kind:
NonImperativeMood: Writes a logical line that
location:
row: 25
row: 33
column: 4
end_location:
row: 27
row: 35
column: 7
fix: ~
parent: ~
- kind:
NonImperativeMood: This method docstring should be written in imperative mood.
location:
row: 72
column: 8
end_location:
row: 72
column: 73
fix: ~
parent: ~

View File

@@ -2,6 +2,8 @@ use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Boolop, Expr, ExprKind};
use crate::ast::hashable::HashableExpr;
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
@@ -13,7 +15,8 @@ pub fn merge_isinstance(checker: &mut Checker, expr: &Expr, op: &Boolop, values:
return;
}
let mut obj_to_types: FxHashMap<String, (usize, FxHashSet<String>)> = FxHashMap::default();
let mut obj_to_types: FxHashMap<HashableExpr, (usize, FxHashSet<HashableExpr>)> =
FxHashMap::default();
for value in values {
let ExprKind::Call { func, args, .. } = &value.node else {
continue;
@@ -25,16 +28,14 @@ pub fn merge_isinstance(checker: &mut Checker, expr: &Expr, op: &Boolop, values:
continue;
};
let (num_calls, matches) = obj_to_types
.entry(obj.to_string())
.entry(obj.into())
.or_insert_with(|| (0, FxHashSet::default()));
*num_calls += 1;
matches.extend(match &types.node {
ExprKind::Tuple { elts, .. } => {
elts.iter().map(std::string::ToString::to_string).collect()
}
ExprKind::Tuple { elts, .. } => elts.iter().map(HashableExpr::from_expr).collect(),
_ => {
vec![types.to_string()]
vec![types.into()]
}
});
}
@@ -42,7 +43,15 @@ pub fn merge_isinstance(checker: &mut Checker, expr: &Expr, op: &Boolop, values:
for (obj, (num_calls, types)) in obj_to_types {
if num_calls > 1 && types.len() > 1 {
checker.diagnostics.push(Diagnostic::new(
violations::ConsiderMergingIsinstance(obj, types.into_iter().sorted().collect()),
violations::ConsiderMergingIsinstance(
unparse_expr(obj.as_expr(), checker.stylist),
types
.iter()
.map(HashableExpr::as_expr)
.map(|expr| unparse_expr(expr, checker.stylist))
.sorted()
.collect(),
),
Range::from_located(expr),
));
}

View File

@@ -1,11 +1,12 @@
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
static CURLY_ESCAPE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\\N\{[^}]+})|([{}])").unwrap());
static CURLY_BRACES: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\\N\{[^}]+})|([{}])").unwrap());
pub fn curly_escape(text: &str) -> String {
// We don't support emojis right now.
CURLY_ESCAPE
// Match all curly braces. This will include named unicode escapes (like
// \N{SNOWMAN}), which we _don't_ want to escape, so take care to preserve them.
CURLY_BRACES
.replace_all(text, |caps: &Captures| {
if let Some(match_) = caps.get(1) {
match_.as_str().to_string()

View File

@@ -14,6 +14,7 @@ mod tests {
use crate::settings;
#[test_case(Rule::PreferTypeError, Path::new("TRY004.py"); "TRY004")]
#[test_case(Rule::VerboseRaise, Path::new("TRY201.py"); "TRY201")]
#[test_case(Rule::TryConsiderElse, Path::new("TRY300.py"); "TRY300")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());

View File

@@ -1,5 +1,7 @@
pub use prefer_type_error::{prefer_type_error, PreferTypeError};
pub use try_consider_else::{try_consider_else, TryConsiderElse};
pub use verbose_raise::{verbose_raise, VerboseRaise};
mod prefer_type_error;
mod try_consider_else;
mod verbose_raise;

View File

@@ -0,0 +1,68 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct VerboseRaise;
);
impl Violation for VerboseRaise {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `raise` without specifying exception name")
}
}
#[derive(Default)]
struct RaiseStatementVisitor<'a> {
raises: Vec<Option<&'a Expr>>,
}
impl<'a, 'b> Visitor<'b> for RaiseStatementVisitor<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
match &stmt.node {
StmtKind::Raise { exc, .. } => self.raises.push(exc.as_ref().map(|expr| &**expr)),
_ => visitor::walk_stmt(self, stmt),
}
}
}
/// TRY201
pub fn verbose_raise(checker: &mut Checker, handlers: &[Excepthandler]) {
for handler in handlers {
// If the handler assigned a name to the exception...
if let ExcepthandlerKind::ExceptHandler {
name: Some(exception_name),
body,
..
} = &handler.node
{
let raises = {
let mut visitor = RaiseStatementVisitor::default();
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor.raises
};
for expr in raises.into_iter().flatten() {
// ...and the raised object is bound to the same name...
if let ExprKind::Name { id, .. } = &expr.node {
if id == exception_name {
checker
.diagnostics
.push(Diagnostic::new(VerboseRaise, Range::from_located(expr)));
}
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
---
source: src/rules/tryceratops/mod.rs
expression: diagnostics
---
- kind:
VerboseRaise: ~
location:
row: 20
column: 14
end_location:
row: 20
column: 15
fix: ~
parent: ~
- kind:
VerboseRaise: ~
location:
row: 44
column: 18
end_location:
row: 44
column: 19
fix: ~
parent: ~
- kind:
VerboseRaise: ~
location:
row: 54
column: 22
end_location:
row: 54
column: 23
fix: ~
parent: ~

View File

@@ -255,10 +255,9 @@ pub struct Options {
"#
)]
/// Avoid automatically removing unused imports in `__init__.py` files. Such
/// imports will still be +flagged, but with a dedicated message
/// suggesting that the import is either added to the module' +`__all__`
/// symbol, or re-exported with a redundant alias (e.g., `import os as
/// os`).
/// imports will still be flagged, but with a dedicated message suggesting
/// that the import is either added to the module's `__all__` symbol, or
/// re-exported with a redundant alias (e.g., `import os as os`).
pub ignore_init_module_imports: Option<bool>,
#[option(
default = "88",

View File

@@ -1,6 +1,5 @@
//! Generate Python source code from an abstract syntax tree (AST).
use std::fmt::{self, Write};
use std::ops::Deref;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Suite, Withitem};
@@ -116,10 +115,6 @@ impl<'a> Generator<'a> {
self.p_if(!std::mem::take(first), s);
}
fn write_fmt(&mut self, f: fmt::Arguments<'_>) {
self.buffer.write_fmt(f).unwrap();
}
pub fn unparse_suite<U>(&mut self, suite: &Suite<U>) {
for stmt in suite {
self.unparse_stmt(stmt);
@@ -928,7 +923,8 @@ impl<'a> Generator<'a> {
self.p_delim(&mut first, ", ");
self.unparse_arg(arg);
if let Some(i) = i.checked_sub(defaults_start) {
write!(self, "={}", &args.defaults[i]);
self.p("=");
self.unparse_expr(&args.defaults[i], precedence::TEST);
}
self.p_if(i + 1 == args.posonlyargs.len(), ", /");
}
@@ -947,7 +943,8 @@ impl<'a> Generator<'a> {
.checked_sub(defaults_start)
.and_then(|i| args.kw_defaults.get(i))
{
write!(self, "={default}");
self.p("=");
self.unparse_expr(default, precedence::TEST);
}
}
if let Some(kwarg) = &args.kwarg {
@@ -960,7 +957,8 @@ impl<'a> Generator<'a> {
fn unparse_arg<U>(&mut self, arg: &Arg<U>) {
self.p(&arg.node.arg);
if let Some(ann) = &arg.node.annotation {
write!(self, ": {}", **ann);
self.p(": ");
self.unparse_expr(ann, precedence::TEST);
}
}

View File

@@ -4984,13 +4984,33 @@ impl Violation for PreferUniqueEnums {
}
}
define_violation!(
pub struct NoUnnecessarySpread;
);
impl Violation for NoUnnecessarySpread {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary spread `**`")
}
}
define_violation!(
pub struct NoUnnecessaryDictKwargs;
);
impl Violation for NoUnnecessaryDictKwargs {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `dict` kwargs")
}
}
define_violation!(
pub struct PreferListBuiltin;
);
impl AlwaysAutofixableViolation for PreferListBuiltin {
#[derive_message_formats]
fn message(&self) -> String {
format!("Prefer `list()` over useless lambda")
format!("Prefer `list` over useless lambda")
}
fn autofix_title(&self) -> String {

View File

@@ -70,6 +70,15 @@ pub fn is_abstract(checker: &Checker, decorator_list: &[Expr]) -> bool {
})
}
/// Returns `true` if a function definition is a `@property`.
pub fn is_property(checker: &Checker, decorator_list: &[Expr]) -> bool {
decorator_list.iter().any(|expr| {
checker.resolve_call_path(expr).map_or(false, |call_path| {
call_path.as_slice() == ["", "property"]
|| call_path.as_slice() == ["functools", "cached_property"]
})
})
}
/// Returns `true` if a function is a "magic method".
pub fn is_magic(name: &str) -> bool {
name.starts_with("__") && name.ends_with("__")
@@ -90,6 +99,11 @@ pub fn is_call(name: &str) -> bool {
name == "__call__"
}
/// Returns `true` if a function is a test one.
pub fn is_test(name: &str) -> bool {
name == "runTest" || name.starts_with("test")
}
/// Returns `true` if a module name indicates public visibility.
fn is_public_module(module_name: &str) -> bool {
!module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))