Compare commits

...

67 Commits

Author SHA1 Message Date
Charlie Marsh
24faabf1f4 Bump version to 0.0.245 2023-02-10 22:15:27 -05:00
Charlie Marsh
9b0a160239 Only update docs on release (#2755) 2023-02-10 22:14:50 -05:00
Charlie Marsh
9fd29e2c54 Mention default in relative-imports doc 2023-02-10 22:12:22 -05:00
Simon Brugman
e83ed0ecba Implement autofix for relative imports (TID252) (#2739) 2023-02-10 22:05:47 -05:00
Charlie Marsh
dadbfea497 Flag private member accesses on calls et al (#2753) 2023-02-10 19:23:22 -05:00
Nick Pope
9f84c497f9 Adjust heading level in rule documentation (#2749) 2023-02-10 19:10:42 -05:00
Martin Fischer
0ec25d1514 Rename dynamically-typed-expression to any-type (#2751) 2023-02-10 19:02:31 -05:00
Charlie Marsh
6a87c99004 Use explicit fields for implicit-namespace-package 2023-02-10 18:09:30 -05:00
Charlie Marsh
c8f60c9588 Improve implicit-namespace-package documentation 2023-02-10 18:06:48 -05:00
Charlie Marsh
113610a8d4 Improve hardcoded-sql-expression documentation 2023-02-10 18:03:01 -05:00
Charlie Marsh
6376e5915e Improve dynamically-typed-expression documentation 2023-02-10 17:55:26 -05:00
Charlie Marsh
3d8fb5be20 Rewrite documentation for yield-in-init (#2748) 2023-02-10 17:49:55 -05:00
Charlie Marsh
0040991778 Respect NO_COLOR flags in --show-source (#2750) 2023-02-10 17:27:40 -05:00
Charlie Marsh
acb70520f8 Add colored environment variables to README (#2746) 2023-02-10 17:06:02 -05:00
Charlie Marsh
6eb9268675 Allow named unicodes in bidirectional escape check (#2710) 2023-02-10 16:59:28 -05:00
Charlie Marsh
e5f5142e3e Improve yield-in-init documentation 2023-02-10 16:47:44 -05:00
Charlie Marsh
98d5ffb817 Fix __init__.py-to-__init__ in documentation 2023-02-10 16:30:36 -05:00
Charlie Marsh
3f20f73413 Use function_type::classify for yield-in-init (#2742) 2023-02-10 16:19:45 -05:00
tomecki
a5e42d2f7c pylint: E0100 yield-in-init (#2716) 2023-02-10 16:15:15 -05:00
Charlie Marsh
0bc1f68111 Only trigger compound statements after select keywords (#2737) 2023-02-10 15:21:06 -05:00
Charlie Marsh
d2b09d77c5 Only validate __all__ bindings for global scope (#2738) 2023-02-10 15:16:21 -05:00
Charlie Marsh
0377834f9f Mark __all__ members as used at end-of-scope (#2733) 2023-02-10 14:32:05 -05:00
Charlie Marsh
3d650f9dd6 Relax conditions in bad-string-format-type (#2731) 2023-02-10 14:25:59 -05:00
Charlie Marsh
a72590ecde Expand S110 and S112 ranges to include entire exception handler (#2729) 2023-02-10 13:27:18 -05:00
Charlie Marsh
812b227334 Avoid flagging typed exceptions in tuples (#2728) 2023-02-10 13:24:45 -05:00
Martin Fischer
6f58717ba4 refactor: Stop including Rule::code() in pycodestyle .snap filenames 2023-02-10 13:15:47 -05:00
Florian Best
8aab96fb9e feat(isort): Implement known-local-folder (#2657) 2023-02-10 13:15:34 -05:00
Nick Pope
9e6f7153a9 Handle more functions that never return in RET503 (#2719) 2023-02-10 12:09:05 -05:00
Peter Pentchev
cda2ff0b18 Handle functions that never return in RET503 (#2701) 2023-02-10 09:28:34 -05:00
Martin Fischer
ec63658250 Disallow rule names starting with avoid-* 2023-02-10 09:25:29 -05:00
Martin Fischer
1a97de0b01 Disallow rule names starting with uses-* 2023-02-10 09:25:29 -05:00
Martin Fischer
1cbe48522e Disallow rule names ending in *-used 2023-02-10 09:25:29 -05:00
Martin Fischer
bfbde537af Disallow rule names starting with do-not-* 2023-02-10 09:25:29 -05:00
Martin Fischer
cba91b758b Add test for rule names 2023-02-10 09:25:29 -05:00
Martin Fischer
0bab642f5a Describe rule naming convention in CONTRIBUTING.md 2023-02-10 09:25:29 -05:00
Martin Fischer
bd09a1819f Drop unused once_cell dependency from ruff_macros 2023-02-10 09:25:29 -05:00
Martin Fischer
682d206992 refactor: Reduce code duplication 2023-02-10 08:24:22 -05:00
Martin Fischer
c32441e4ab refactor: Use format! keyword arguments 2023-02-10 08:24:22 -05:00
Martin Fischer
6f16f1c39b refactor: Reduce code duplication 2023-02-10 08:24:22 -05:00
Martin Fischer
9011456aa1 refactor: Simplify attribute handling in rule_code_prefix
if_all_same(codes.values().cloned()).unwrap_or_default()

was quite unreadable because it wasn't obvious that codes.values() are
the prefixes. It's better to introduce another Map rather than having
Maps within Maps.
2023-02-10 08:24:22 -05:00
Martin Fischer
fa191cceeb refactor: Avoid implicit precondition 2023-02-10 08:24:22 -05:00
Charlie Marsh
ac6c3affdd Remove public Rust API (#2709) 2023-02-09 23:16:49 -05:00
Charlie Marsh
9a018c1650 Import AutofixKind from violation 2023-02-09 23:06:02 -05:00
Charlie Marsh
0aef5c67a3 Remove src/registry.rs 2023-02-09 23:04:28 -05:00
Charlie Marsh
a048594416 Gate Path.readlink() behind Python 3.9+ guard (#2708) 2023-02-09 22:57:31 -05:00
Charlie Marsh
5437f1299b Remove lifetimes from Printer (#2704) 2023-02-09 21:44:15 -05:00
Charlie Marsh
41c0608a69 Add test module a test-only module (#2703) 2023-02-09 21:28:10 -05:00
messense
eb0d42187f Manage LibCST and RustPython with cargo workspace dependencies (#2700) 2023-02-09 20:49:50 -05:00
Colin Delahunty
48daa0f0ca [pylint]: bad-string-format-type (#2572) 2023-02-09 20:08:56 -05:00
Charlie Marsh
417fe4355f Add colors to statistics output (#2699) 2023-02-09 19:40:29 -05:00
Florian Best
a129181407 feat(cli): let --statistics show fixable codes (#2659) 2023-02-09 19:36:31 -05:00
Matt Oberle
fc628de667 Implement bandit's 'hardcoded-sql-expressions' S608 (#2698)
This is an attempt to implement `bandit` rule `B608` (renamed here `S608`).
- https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html

The rule inspects strings constructed via `+`, `%`, `.format`, and `f""`.

- `+` and `%` via `BinOp`
- `.format` via `Call`
- `f""` via `JoinedString`

Any SQL-ish strings that use Python string formatting are flagged.

The expressions and targeted expression types for the rule come from here:
- 7104b336d3/bandit/plugins/injection_sql.py

> Related Issue: https://github.com/charliermarsh/ruff/issues/1646
2023-02-09 19:28:17 -05:00
Charlie Marsh
9e2418097c Run cargo dev generate-all 2023-02-09 19:14:02 -05:00
Charlie Marsh
d4e5639aaf Add flake8-pyi to CONTRIBUTING.md 2023-02-09 19:04:55 -05:00
Steve Dignam
67e58a024a Add flake8-pyi with one rule (#2682)
Add basic scaffold for [flake8-pyi](https://github.com/PyCQA/flake8-pyi) and the first rule, Y001

rel: https://github.com/charliermarsh/ruff/issues/848
2023-02-09 19:03:11 -05:00
Charlie Marsh
233be0e074 Suppress parse errors with explicit # noqa: E999 directives (#2697) 2023-02-09 18:24:19 -05:00
Nick Pope
7750087f56 Remove duplicate documentation for TRY002 (#2692) 2023-02-09 12:08:00 -05:00
Charlie Marsh
7d5fb0de8a Add documentation for mccabe, isort, and flake8-annotations (#2691) 2023-02-09 11:56:18 -05:00
Charlie Marsh
8a98cfc4b8 Treat re-exported annotations as used-at-runtime (#2689) 2023-02-09 11:22:15 -05:00
Charlie Marsh
54d1719424 Hide rule configuration settings on CLI (#2687) 2023-02-09 11:13:04 -05:00
Charlie Marsh
0f622f0126 Upgrade RustPython to pull in newline-handling optimizations (#2688) 2023-02-09 11:12:43 -05:00
Charlie Marsh
739a92e99d Implement compound-statements (E701, E702, E703, E704) (#2680) 2023-02-08 22:57:39 -05:00
Charlie Marsh
5a07c9f57c Only include rule links once in README (#2678) 2023-02-08 21:48:05 -05:00
Colin Delahunty
31027497c6 [flake8-bandit]: try-except-continue (#2674) 2023-02-08 21:44:01 -05:00
Charlie Marsh
dabfdf718e Mark flake8-simplify rules as unfixable in non-fixable cases (#2676) 2023-02-08 21:28:28 -05:00
Charlie Marsh
5829bae976 Support callable decorators in classmethod_decorators et al (#2675) 2023-02-08 21:11:36 -05:00
Charlie Marsh
ff3665a24b Mark RUF005 as fixable 2023-02-08 18:02:33 -05:00
250 changed files with 5417 additions and 1236 deletions

View File

@@ -1,12 +1,8 @@
name: mkdocs
on:
push:
paths:
- README.md
- mkdocs.template.yml
- .github/workflows/docs.yaml
branches: [main]
release:
types: [published]
workflow_dispatch:
jobs:

View File

@@ -1,5 +1,13 @@
# Breaking Changes
## 0.0.245
### Ruff's public `check` method was removed ([#2709](https://github.com/charliermarsh/ruff/pull/2709))
Previously, Ruff exposed a `check` method as a public Rust API. This method was used by few,
if any clients, and was not well documented or supported. As such, it has been removed, with
the intention of adding a stable public API in the future.
## 0.0.238
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/charliermarsh/ruff/pull/2312))

View File

@@ -22,6 +22,9 @@ As a concrete example: consider taking on one of the rules from the [`tryceratop
plugin, and looking to the originating [Python source](https://github.com/guilatrova/tryceratops)
for guidance.
Alternatively, we've started work on the [`flake8-pyi`](https://github.com/charliermarsh/ruff/issues/848)
plugin (see the [Python source](https://github.com/PyCQA/flake8-pyi)) -- another good place to start.
### Prerequisites
Ruff is written in Rust. You'll need to install the
@@ -91,15 +94,16 @@ At time of writing, the repository includes the following crates:
At a high level, the steps involved in adding a new lint rule are as follows:
1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
2. In that file, define a violation struct. You can grep for `define_violation!` to see examples.
3. Map the violation struct to a rule code in `crates/ruff/src/registry.rs` (e.g., `E402`).
4. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast.rs` (for AST-based
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention).
2. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
3. In that file, define a violation struct. You can grep for `define_violation!` to see examples.
4. Map the violation struct to a rule code in `crates/ruff/src/registry.rs` (e.g., `E402`).
5. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast.rs` (for AST-based
checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks), `crates/ruff/src/checkers/lines.rs`
(for text-based checks), or `crates/ruff/src/checkers/filesystem.rs` (for filesystem-based
checks).
5. Add a test fixture.
6. Update the generated files (documentation and generated code).
6. Add a test fixture.
7. Update the generated files (documentation and generated code).
To define the violation, start by creating a dedicated file for your rule under the appropriate
rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
@@ -129,6 +133,17 @@ generated snapshot, then commit the snapshot file alongside the rest of your cha
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
#### Rule naming convention
The rule name should make sense when read as "allow *rule-name*" or "allow *rule-name* items".
This implies that rule names:
* should state the bad thing being checked for
* should not contain instructions on what you what you should use instead
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
### Example: Adding a new configuration option
Ruff's user-facing settings live in a few different places.

21
Cargo.lock generated
View File

@@ -747,7 +747,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -1896,7 +1896,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"anyhow",
"bisection",
@@ -1952,7 +1952,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1988,7 +1988,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -2008,10 +2008,9 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"itertools",
"once_cell",
"proc-macro2",
"quote",
"syn",
@@ -2020,7 +2019,7 @@ dependencies = [
[[package]]
name = "ruff_python"
version = "0.0.244"
version = "0.0.245"
dependencies = [
"once_cell",
"regex",
@@ -2072,7 +2071,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=adc23253e4b58980b407ba2760dbe61681d752fc#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython.git?rev=d94d0ac72072eb60bd9363e69b96ff1d5eb401b3#d94d0ac72072eb60bd9363e69b96ff1d5eb401b3"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -2081,7 +2080,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=adc23253e4b58980b407ba2760dbe61681d752fc#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython.git?rev=d94d0ac72072eb60bd9363e69b96ff1d5eb401b3#d94d0ac72072eb60bd9363e69b96ff1d5eb401b3"
dependencies = [
"ascii",
"bitflags",
@@ -2106,7 +2105,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=adc23253e4b58980b407ba2760dbe61681d752fc#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython.git?rev=d94d0ac72072eb60bd9363e69b96ff1d5eb401b3#d94d0ac72072eb60bd9363e69b96ff1d5eb401b3"
dependencies = [
"bincode",
"bitflags",
@@ -2123,7 +2122,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=adc23253e4b58980b407ba2760dbe61681d752fc#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython.git?rev=d94d0ac72072eb60bd9363e69b96ff1d5eb401b3#d94d0ac72072eb60bd9363e69b96ff1d5eb401b3"
dependencies = [
"ahash",
"anyhow",

View File

@@ -2,6 +2,11 @@
members = ["crates/*"]
default-members = ["crates/ruff", "crates/ruff_cli"]
[workspace.dependencies]
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d94d0ac72072eb60bd9363e69b96ff1d5eb401b3" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "d94d0ac72072eb60bd9363e69b96ff1d5eb401b3" }
[profile.release]
panic = "abort"
lto = "thin"

25
LICENSE
View File

@@ -245,6 +245,31 @@ are:
SOFTWARE.
"""
- flake8-pyi, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2016 Łukasz Langa
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.
"""
- flake8-print, licensed as follows:
"""
MIT License

138
README.md
View File

@@ -145,6 +145,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-pyi (PYI)](#flake8-pyi-pyi)
1. [flake8-pytest-style (PT)](#flake8-pytest-style-pt)
1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-return (RET)](#flake8-return-ret)
@@ -230,7 +231,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.244'
rev: 'v0.0.245'
hooks:
- id: ruff
```
@@ -456,14 +457,6 @@ File selection:
--respect-gitignore Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude Enforce exclusions, even for paths passed to Ruff directly on the command-line
Rule configuration:
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated rules and automatic formatting
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
Miscellaneous:
-n, --no-cache
Disable cache reads
@@ -631,17 +624,17 @@ for more.
By default, Ruff exits with the following status codes:
- `0` if no violations were found, or if all present violations were fixed automatically.
- `1` if violations were found.
- `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
* `0` if no violations were found, or if all present violations were fixed automatically.
* `1` if violations were found.
* `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
This convention mirrors that of tools like ESLint, Prettier, and RuboCop.
Ruff supports two command-line flags that alter its exit code behavior:
- `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
* `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
Note that Ruff will still exit with a status code of `2` if it terminates abnormally.
- `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
* `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
found, _even if_ all such violations were fixed automatically. Note that the use of
`--exit-non-zero-on-fix` can result in a non-zero exit code even if no violations remain after
autofixing.
@@ -671,7 +664,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| ---- | ---- | ------- | --- |
| F401 | unused-import | `{name}` imported but unused; consider adding to `__all__` or using a redundant alias | 🛠 |
| F402 | import-shadowed-by-loop-var | Import `{name}` from line {line} shadowed by loop variable | |
| F403 | import-star-used | `from {name} import *` used; unable to detect undefined names | |
| F403 | import-star | `from {name} import *` used; unable to detect undefined names | |
| F404 | late-future-import | `from __future__` imports must occur at the beginning of the file | |
| F405 | import-star-usage | `{name}` may be undefined, or defined from star imports: {sources} | |
| F406 | import-star-not-permitted | `from {name} import *` only allowed at module level | |
@@ -725,13 +718,17 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
| E401 | multiple-imports-on-one-line | Multiple imports on one line | |
| E402 | module-import-not-at-top-of-file | Module level import not at top of file | |
| E501 | line-too-long | Line too long ({length} > {limit} characters) | |
| E701 | multiple-statements-on-one-line-colon | Multiple statements on one line (colon) | |
| E702 | multiple-statements-on-one-line-semicolon | Multiple statements on one line (semicolon) | |
| E703 | useless-semicolon | Statement ends with an unnecessary semicolon | |
| E704 | multiple-statements-on-one-line-def | Multiple statements on one line (def) | |
| E711 | none-comparison | Comparison to `None` should be `cond is None` | 🛠 |
| E712 | true-false-comparison | Comparison to `True` should be `cond is True` | 🛠 |
| E713 | not-in-test | Test for membership should be `not in` | 🛠 |
| E714 | not-is-test | Test for object identity should be `is not` | 🛠 |
| E721 | type-comparison | Do not compare types, use `isinstance()` | |
| E722 | do-not-use-bare-except | Do not use bare `except` | |
| E731 | do-not-assign-lambda | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E722 | bare-except | Do not use bare `except` | |
| E731 | lambda-assignment | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E741 | ambiguous-variable-name | Ambiguous variable name: `{name}` | |
| E742 | ambiguous-class-name | Ambiguous class name: `{name}` | |
| E743 | ambiguous-function-name | Ambiguous function name: `{name}` | |
@@ -752,7 +749,7 @@ For more, see [mccabe](https://pypi.org/project/mccabe/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C901 | function-is-too-complex | `{name}` is too complex ({complexity}) | |
| C901 | [function-is-too-complex](https://github.com/charliermarsh/ruff/blob/main/docs/rules/function-is-too-complex.md) | `{name}` is too complex ({complexity}) | |
### isort (I)
@@ -760,8 +757,8 @@ For more, see [isort](https://pypi.org/project/isort/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | unsorted-imports | Import block is un-sorted or un-formatted | 🛠 |
| I002 | missing-required-import | Missing required import: `{name}` | 🛠 |
| I001 | [unsorted-imports](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unsorted-imports.md) | Import block is un-sorted or un-formatted | 🛠 |
| I002 | [missing-required-import](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-required-import.md) | Missing required import: `{name}` | 🛠 |
### pep8-naming (N)
@@ -815,8 +812,8 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D213 | multi-line-summary-second-line | Multi-line docstring summary should start at the second line | 🛠 |
| D214 | section-not-over-indented | Section is over-indented ("{name}") | 🛠 |
| D215 | section-underline-not-over-indented | Section underline is over-indented ("{name}") | 🛠 |
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
| D300 | triple-single-quotes | Use """triple double quotes""" | |
| D301 | escape-sequence-in-docstring | Use r""" if any backslashes in a docstring | |
| D400 | ends-in-period | First line should end with a period | 🛠 |
| D401 | non-imperative-mood | First line of docstring should be in imperative mood: "{first_line}" | |
| D402 | no-signature | First line should not be the function's signature | |
@@ -903,17 +900,17 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ANN001 | missing-type-function-argument | Missing type annotation for function argument `{name}` | |
| ANN002 | missing-type-args | Missing type annotation for `*{name}` | |
| ANN003 | missing-type-kwargs | Missing type annotation for `**{name}` | |
| ANN101 | missing-type-self | Missing type annotation for `{name}` in method | |
| ANN102 | missing-type-cls | Missing type annotation for `{name}` in classmethod | |
| ANN201 | missing-return-type-public-function | Missing return type annotation for public function `{name}` | |
| ANN202 | missing-return-type-private-function | Missing return type annotation for private function `{name}` | |
| ANN204 | missing-return-type-special-method | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | missing-return-type-static-method | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | missing-return-type-class-method | Missing return type annotation for classmethod `{name}` | |
| ANN401 | dynamically-typed-expression | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
| ANN001 | [missing-type-function-argument](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-function-argument.md) | Missing type annotation for function argument `{name}` | |
| ANN002 | [missing-type-args](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-args.md) | Missing type annotation for `*{name}` | |
| ANN003 | [missing-type-kwargs](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-kwargs.md) | Missing type annotation for `**{name}` | |
| ANN101 | [missing-type-self](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-self.md) | Missing type annotation for `{name}` in method | |
| ANN102 | [missing-type-cls](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-cls.md) | Missing type annotation for `{name}` in classmethod | |
| ANN201 | [missing-return-type-public-function](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-public-function.md) | Missing return type annotation for public function `{name}` | |
| ANN202 | [missing-return-type-private-function](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-private-function.md) | Missing return type annotation for private function `{name}` | |
| ANN204 | [missing-return-type-special-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-special-method.md) | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | [missing-return-type-static-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-static-method.md) | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | [missing-return-type-class-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-class-method.md) | Missing return type annotation for classmethod `{name}` | |
| ANN401 | [any-type](https://github.com/charliermarsh/ruff/blob/main/docs/rules/any-type.md) | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
### flake8-bandit (S)
@@ -921,8 +918,8 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| S101 | assert-used | Use of `assert` detected | |
| S102 | exec-used | Use of `exec` detected | |
| S101 | assert | Use of `assert` detected | |
| S102 | exec-builtin | Use of `exec` detected | |
| S103 | bad-file-permissions | `os.chmod` setting a permissive mask `{mask:#o}` on file or directory | |
| S104 | hardcoded-bind-all-interfaces | Possible binding to all interfaces | |
| S105 | hardcoded-password-string | Possible hardcoded password: "{}" | |
@@ -930,12 +927,14 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| S107 | hardcoded-password-default | Possible hardcoded password: "{}" | |
| S108 | hardcoded-temp-file | Probable insecure usage of temporary file or directory: "{}" | |
| S110 | try-except-pass | `try`-`except`-`pass` detected, consider logging the exception | |
| S112 | try-except-continue | `try`-`except`-`continue` detected, consider logging the exception | |
| S113 | request-without-timeout | Probable use of requests call with timeout set to `{value}` | |
| S324 | hashlib-insecure-hash-function | Probable use of insecure hash functions in `hashlib`: "{}" | |
| S501 | request-with-no-cert-validation | Probable use of `{string}` call with `verify=False` disabling SSL certificate checks | |
| 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. | |
| S608 | [hardcoded-sql-expression](https://github.com/charliermarsh/ruff/blob/main/docs/rules/hardcoded-sql-expression.md) | Possible SQL injection vector through string-based query construction: "{}" | |
| 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. | |
@@ -972,13 +971,13 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) on PyPI
| B008 | function-call-argument-default | Do not perform function call `{name}` in argument defaults | |
| B009 | get-attr-with-constant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | set-attr-with-constant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B011 | do-not-assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B011 | assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B012 | jump-statement-in-finally | `{name}` inside `finally` blocks cause exceptions to be silenced | |
| B013 | redundant-tuple-in-exception-handler | A length-one tuple literal is redundant. Write `except {name}` instead of `except ({name},)`. | 🛠 |
| B014 | duplicate-handler-exception | Exception handler with duplicate exception: `{name}` | 🛠 |
| B015 | useless-comparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | |
| B016 | cannot-raise-literal | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
| [B017](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | [assert-raises-exception](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | `assertRaises(Exception)` should be considered evil | |
| B017 | [assert-raises-exception](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | `assertRaises(Exception)` should be considered evil | |
| B018 | useless-expression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | cached-instance-method | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | loop-variable-overrides-iterator | Loop control variable `{name}` overrides iterable it iterates | |
@@ -1097,7 +1096,7 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ICN001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | [unconventional-import-alias](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | `{name}` should be imported as `{asname}` | |
| ICN001 | [unconventional-import-alias](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | `{name}` should be imported as `{asname}` | |
### flake8-logging-format (G)
@@ -1120,7 +1119,7 @@ For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [INP001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | [implicit-namespace-package](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
| INP001 | [implicit-namespace-package](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-pie (PIE)
@@ -1145,6 +1144,14 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
| T201 | print-found | `print` found | |
| T203 | p-print-found | `pprint` found | |
### flake8-pyi (PYI)
For more, see [flake8-pyi](https://pypi.org/project/flake8-pyi/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PYI001 | [prefix-type-params](https://github.com/charliermarsh/ruff/blob/main/docs/rules/prefix-type-params.md) | Name of private `{kind}` must start with _ | |
### flake8-pytest-style (PT)
For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) on PyPI.
@@ -1183,10 +1190,10 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [Q000](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | [bad-quotes-inline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | Double quotes found but single quotes preferred | 🛠 |
| [Q001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | [bad-quotes-multiline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | Double quote multiline found but single quotes preferred | 🛠 |
| [Q002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | [bad-quotes-docstring](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | Double quote docstring found but single quotes preferred | 🛠 |
| [Q003](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | [avoid-quote-escape](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | Change outer quotes to avoid escaping inner quotes | 🛠 |
| Q000 | [bad-quotes-inline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | Double quotes found but single quotes preferred | 🛠 |
| Q001 | [bad-quotes-multiline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | Double quote multiline found but single quotes preferred | 🛠 |
| Q002 | [bad-quotes-docstring](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | Double quote docstring found but single quotes preferred | 🛠 |
| Q003 | [avoidable-escaped-quote](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoidable-escaped-quote.md) | Change outer quotes to avoid escaping inner quotes | 🛠 |
### flake8-return (RET)
@@ -1211,7 +1218,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on Py
| ---- | ---- | ------- | --- |
| SIM101 | duplicate-isinstance-call | Multiple `isinstance` calls for `{name}`, merge into a single call | 🛠 |
| SIM102 | nested-if-statements | Use a single `if` statement instead of nested `if` statements | 🛠 |
| SIM103 | return-bool-condition-directly | Return the condition `{cond}` directly | 🛠 |
| SIM103 | return-bool-condition-directly | Return the condition `{condition}` directly | 🛠 |
| SIM105 | use-contextlib-suppress | Use `contextlib.suppress({exception})` instead of try-except-pass | |
| SIM107 | return-in-try-except-finally | Don't use `return` in `try`/`except` and `finally` | |
| SIM108 | use-ternary-operator | Use ternary operator `{contents}` instead of if-else-block | 🛠 |
@@ -1242,7 +1249,7 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | banned-api | `{name}` is banned: {message} | |
| TID252 | relative-imports | Relative imports from parent modules are banned | |
| TID252 | [relative-imports](https://github.com/charliermarsh/ruff/blob/main/docs/rules/relative-imports.md) | Relative imports from parent modules are banned | 🛠 |
### flake8-type-checking (TCH)
@@ -1306,7 +1313,7 @@ For more, see [eradicate](https://pypi.org/project/eradicate/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ERA001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | [commented-out-code](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | Found commented-out code | 🛠 |
| ERA001 | [commented-out-code](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | Found commented-out code | 🛠 |
### pandas-vet (PD)
@@ -1353,13 +1360,15 @@ For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLE0100 | [yield-in-init](https://github.com/charliermarsh/ruff/blob/main/docs/rules/yield-in-init.md) | `__init__` method is a generator | |
| PLE0117 | nonlocal-without-binding | Nonlocal name `{name}` found without binding | |
| PLE0118 | used-prior-global-declaration | Name `{name}` is used prior to global declaration on line {line} | |
| PLE0604 | invalid-all-object | Invalid object in `__all__`, must contain only strings | |
| PLE0605 | invalid-all-format | Invalid format for `__all__`, must be `tuple` or `list` | |
| PLE1142 | await-outside-async | `await` should be used within an async function | |
| PLE1307 | [bad-string-format-type](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-string-format-type.md) | Format type does not match argument type | |
| PLE1310 | bad-str-strip-call | String `{strip}` call contains duplicate characters (did you mean `{removal}`?) | |
| PLE2502 | bidirectional-unicode | Avoid using bidirectional unicode | |
| PLE2502 | bidirectional-unicode | Contains control characters that can permit obfuscated code | |
#### Refactor (PLR)
@@ -1389,7 +1398,7 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [TRY002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | [raise-vanilla-class](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | Create your own exception | |
| TRY002 | [raise-vanilla-class](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | Create your own exception | |
| TRY003 | raise-vanilla-args | Avoid specifying long messages outside the exception class | |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY200 | reraise-no-cause | Use `raise from` to specify exception cause | |
@@ -1422,7 +1431,7 @@ For more, see [flake8-self](https://pypi.org/project/flake8-self/) on PyPI.
| RUF002 | ambiguous-unicode-character-docstring | Docstring contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF003 | ambiguous-unicode-character-comment | Comment contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF004 | keyword-argument-before-star-argument | Keyword argument `{name}` must come after starred arguments | |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | 🛠 |
| RUF100 | unused-noqa | Unused blanket `noqa` directive | 🛠 |
<!-- End auto-generated sections. -->
@@ -1704,6 +1713,7 @@ natively, including:
* [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420)
* [flake8-pie](https://pypi.org/project/flake8-pie/)
* [flake8-print](https://pypi.org/project/flake8-print/)
* [flake8-pyi](https://pypi.org/project/flake8-pyi/)
* [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
* [flake8-quotes](https://pypi.org/project/flake8-quotes/)
* [flake8-raise](https://pypi.org/project/flake8-raise/)
@@ -1975,6 +1985,15 @@ unfixable = ["B", "SIM", "TRY", "RUF"]
If you find a case where Ruff's autofix breaks your code, please file an Issue!
### How can I disable Ruff's color output?
Ruff's color output is powered by the [`colored`](https://crates.io/crates/colored) crate, which
attempts to automatically detect whether the output stream supports color. However, you can force
colors off by setting the `NO_COLOR` environment variable to any value (e.g., `NO_COLOR=1`).
[`colored`](https://crates.io/crates/colored) also supports the the `CLICOLOR` and `CLICOLOR_FORCE`
environment variables (see the [spec](https://bixense.com/clicolors/)).
<!-- End section: FAQ -->
## Contributing
@@ -3396,7 +3415,7 @@ alias (e.g., `import A as B`) to wrap such that every line contains
exactly one member. For example, this formatting would be retained,
rather than condensing to a single line:
```py
```python
from .utils import (
test_directory as test_directory,
test_id as test_id
@@ -3458,6 +3477,25 @@ known-first-party = ["src"]
---
#### [`known-local-folder`](#known-local-folder)
A list of modules to consider being a local folder.
Generally, this is reserved for relative
imports (from . import module).
**Default value**: `[]`
**Type**: `list[str]`
**Example usage**:
```toml
[tool.ruff.isort]
known-local-folder = ["src"]
```
---
#### [`known-third-party`](#known-third-party)
A list of modules to consider third-party, regardless of whether they

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.244"
version = "0.0.245"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -30,7 +30,7 @@ globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
libcst = { workspace = true }
log = { version = "0.4.17" }
natord = { version = "1.0.9" }
nohash-hasher = { version = "0.2.0" }
@@ -39,11 +39,11 @@ 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.244", path = "../ruff_macros" }
ruff_python = { version = "0.0.244", path = "../ruff_python" }
ruff_macros = { version = "0.0.245", path = "../ruff_macros" }
ruff_python = { version = "0.0.245", path = "../ruff_python" }
rustc-hash = { version = "1.1.0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
rustpython-common = { workspace = true }
rustpython-parser = { workspace = true }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }

View File

@@ -0,0 +1,4 @@
avoid-*
do-not-*
uses-*
*-used

View File

@@ -0,0 +1,29 @@
try:
pass
except Exception:
continue
try:
pass
except:
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except ValueError:
continue
try:
pass
except (ValueError,):
continue

View File

@@ -0,0 +1,95 @@
# single-line failures
query1 = "SELECT %s FROM table" % (var,) # bad
query2 = "SELECT var FROM " + table
query3 = "SELECT " + val + " FROM " + table
query4 = "SELECT {} FROM table;".format(var)
query5 = f"SELECT * FROM table WHERE var = {var}"
query6 = "DELETE FROM table WHERE var = %s" % (var,)
query7 = "DELETE FROM table WHERE VAR = " + var
query8 = "DELETE FROM " + table + "WHERE var = " + var
query9 = "DELETE FROM table WHERE var = {}".format(var)
query10 = f"DELETE FROM table WHERE var = {var}"
query11 = "INSERT INTO table VALUES (%s)" % (var,)
query12 = "INSERT INTO TABLE VALUES (" + var + ")"
query13 = "INSERT INTO {} VALUES ({})".format(table, var)
query14 = f"INSERT INTO {table} VALUES var = {var}"
query15 = "UPDATE %s SET var = %s" % (table, var)
query16 = "UPDATE " + table + " SET var = " + var
query17 = "UPDATE {} SET var = {}".format(table, var)
query18 = f"UPDATE {table} SET var = {var}"
query19 = "select %s from table" % (var,)
query20 = "select var from " + table
query21 = "select " + val + " from " + table
query22 = "select {} from table;".format(var)
query23 = f"select * from table where var = {var}"
query24 = "delete from table where var = %s" % (var,)
query25 = "delete from table where var = " + var
query26 = "delete from " + table + "where var = " + var
query27 = "delete from table where var = {}".format(var)
query28 = f"delete from table where var = {var}"
query29 = "insert into table values (%s)" % (var,)
query30 = "insert into table values (" + var + ")"
query31 = "insert into {} values ({})".format(table, var)
query32 = f"insert into {table} values var = {var}"
query33 = "update %s set var = %s" % (table, var)
query34 = "update " + table + " set var = " + var
query35 = "update {} set var = {}".format(table, var)
query36 = f"update {table} set var = {var}"
# multi-line failures
def query37():
return """
SELECT *
FROM table
WHERE var = %s
""" % var
def query38():
return """
SELECT *
FROM TABLE
WHERE var =
""" + var
def query39():
return """
SELECT *
FROM table
WHERE var = {}
""".format(var)
def query40():
return f"""
SELECT *
FROM table
WHERE var = {var}
"""
def query41():
return (
"SELECT *"
"FROM table"
f"WHERE var = {var}"
)
# # cursor-wrapped failures
query42 = cursor.execute("SELECT * FROM table WHERE var = %s" % var)
query43 = cursor.execute(f"SELECT * FROM table WHERE var = {var}")
query44 = cursor.execute("SELECT * FROM table WHERE var = {}".format(var))
query45 = cursor.executemany("SELECT * FROM table WHERE var = %s" % var, [])
# # pass
query = "SELECT * FROM table WHERE id = 1"
query = "DELETE FROM table WHERE id = 1"
query = "INSERT INTO table VALUES (1)"
query = "UPDATE table SET id = 1"
cursor.execute('SELECT * FROM table WHERE id = %s', var)
cursor.execute('SELECT * FROM table WHERE id = 1')
cursor.executemany('SELECT * FROM table WHERE id = %s', [var, var2])

View File

@@ -0,0 +1,13 @@
from typing import ParamSpec, TypeVar, TypeVarTuple
T = TypeVar("T") # OK
TTuple = TypeVarTuple("TTuple") # OK
P = ParamSpec("P") # OK
_T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK

View File

@@ -0,0 +1,13 @@
from typing import ParamSpec, TypeVar, TypeVarTuple
T = TypeVar("T") # Error: TypeVars in stubs must start with _
TTuple = TypeVarTuple("TTuple") # Error: TypeVarTuples must also start with _
P = ParamSpec("P") # Error: ParamSpecs must start with _
_T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK

View File

@@ -1,3 +1,14 @@
import builtins
import os
import posix
from posix import abort
import sys as std_sys
import _thread
import _winapi
import pytest
from pytest import xfail as py_xfail
###
# Errors
###
@@ -39,6 +50,20 @@ def x(y):
print() # error
# A nonexistent function
def func_unknown(x):
if x > 0:
return False
no_such_function() # error
# A function that does return the control
def func_no_noreturn(x):
if x > 0:
return False
print("", end="") # error
###
# Non-errors
###
@@ -123,3 +148,106 @@ def prompts(self, foo):
for x in foo:
yield x
yield x + 1
# Functions that never return
def noreturn_exit(x):
if x > 0:
return 1
exit()
def noreturn_quit(x):
if x > 0:
return 1
quit()
def noreturn_builtins_exit(x):
if x > 0:
return 1
builtins.exit()
def noreturn_builtins_quit(x):
if x > 0:
return 1
builtins.quit()
def noreturn_os__exit(x):
if x > 0:
return 1
os._exit(0)
def noreturn_os_abort(x):
if x > 0:
return 1
os.abort()
def noreturn_posix__exit():
if x > 0:
return 1
posix._exit()
def noreturn_posix_abort():
if x > 0:
return 1
posix.abort()
def noreturn_posix_abort_2():
if x > 0:
return 1
abort()
def noreturn_sys_exit():
if x > 0:
return 1
std_sys.exit(0)
def noreturn__thread_exit():
if x > 0:
return 1
_thread.exit(0)
def noreturn__winapi_exitprocess():
if x > 0:
return 1
_winapi.ExitProcess(0)
def noreturn_pytest_exit():
if x > 0:
return 1
pytest.exit("oof")
def noreturn_pytest_fail():
if x > 0:
return 1
pytest.fail("oof")
def noreturn_pytest_skip():
if x > 0:
return 1
pytest.skip("oof")
def noreturn_pytest_xfail():
if x > 0:
return 1
pytest.xfail("oof")
def noreturn_pytest_xfail_2():
if x > 0:
return 1
py_xfail("oof")

View File

@@ -33,6 +33,8 @@ class Foo(metaclass=BazMeta):
def get_bar():
if self.bar._private: # SLF001
return None
if self.bar()._private: # SLF001
return None
return self.bar
def public_func(self):
@@ -51,9 +53,11 @@ print(foo.public_thing)
print(foo.public_func())
print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._private_thing) # SLF001
print(foo.__really_private_thing) # SLF001
print(foo._private_func()) # SLF001
print(foo.__really_private_func(1)) # SLF001
print(foo.bar._private) # SLF001
print(foo()._private_thing) # SLF001

View File

@@ -1,12 +1,30 @@
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
# OK
import other
import other.example
from other import example
# TID252
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
from .parent import hello
from .\
parent import \
hello_world
from \
..parent\
import \
world_hello
# TID252 (without autofix; too many levels up)
from ..... import ultragrantparent
from ...... import ultragrantparent
from ....... import ultragrantparent
from ......... import ultragrantparent
from ........................... import ultragrantparent
from .....parent import ultragrantparent
from .........parent import ultragrantparent
from ...........................parent import ultragrantparent

View File

@@ -0,0 +1,6 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List
__all__ = ("List",)

View File

@@ -3,3 +3,4 @@ line-length = 88
[tool.ruff.isort]
lines-after-imports = 3
known-local-folder = ["ruff"]

View File

@@ -1,4 +1,5 @@
import sys
import ruff
import leading_prefix
import os
from . import leading_prefix

View File

@@ -0,0 +1,46 @@
#: E701:1:5
if a: a = False
#: E701:1:40
if not header or header[:6] != 'bytes=': return
#: E702:1:10
a = False; b = True
#: E702:1:17
import bdist_egg; bdist_egg.write_safety_flag(cmd.egg_info, safe)
#: E703:1:13
import shlex;
#: E702:1:9 E703:1:23
del a[:]; a.append(42);
#: E704:1:1
def f(x): return 2
#: E704:1:1
async def f(x): return 2
#: E704:1:1 E271:1:6
async def f(x): return 2
#: E704:1:1 E226:1:19
def f(x): return 2*x
#: E704:2:5 E226:2:23
while all is round:
def f(x): return 2*x
#: E704:1:8 E702:1:11 E703:1:14
if True: x; y;
#: E701:1:8
if True: lambda a: b
#: E701:1:10
if a := 1: pass
# E701:1:4 E701:2:18 E701:3:8
try: lambda foo: bar
except ValueError: pass
finally: pass
# E701:1:7
class C: pass
# E701:1:7
with C(): pass
# E701:1:14
async with C(): pass
#:
lambda a: b
#:
a: List[str] = []
#:
if a := 1:
pass

View File

@@ -0,0 +1,4 @@
"""Test: late-binding of `__all__`."""
__all__ = ("bar",)
from foo import bar, baz

View File

@@ -0,0 +1,7 @@
__all__ = ["foo"]
foo = 1
def bar():
pass

View File

@@ -0,0 +1,57 @@
# Errors
print("foo %(foo)d bar %(bar)d" % {"foo": "1", "bar": "2"})
"foo %e bar %s" % ("1", 2)
"%d" % "1"
"%o" % "1"
"%(key)d" % {"key": "1"}
"%x" % 1.1
"%(key)x" % {"key": 1.1}
"%d" % []
"%d" % ([],)
"%(key)d" % {"key": []}
print("%d" % ("%s" % ("nested",),))
"%d" % ((1, 2, 3),)
# False negatives
WORD = "abc"
"%d" % WORD
"%d %s" % (WORD, WORD)
VALUES_TO_FORMAT = (1, "2", 3.0)
"%d %d %f" % VALUES_TO_FORMAT
# OK
"%d %s %f" % VALUES_TO_FORMAT
"%s" % "1"
"%s %s %s" % ("1", 2, 3.5)
print("%d %d"
%
(1, 1.1))
"%s" % 1
"%d" % 1
"%f" % 1
"%s" % 1
"%(key)s" % {"key": 1}
"%d" % 1
"%(key)d" % {"key": 1}
"%f" % 1
"%(key)f" % {"key": 1}
"%d" % 1.1
"%(key)d" % {"key": 1.1}
"%s" % []
"%(key)s" % {"key": []}
"%s" % None
"%(key)s" % {"key": None}
print("%s" % ("%s" % ("nested",),))
print("%s" % ("%d" % (5,),))
"%d %d" % "1"
"%d" "%d" % "1"
"-%f" % time.time()
"%r" % (object['dn'],)
r'\%03o' % (ord(c),)
('%02X' % int(_) for _ in o)
"%s;range=%d-*" % (attr, upper + 1)
"%d" % (len(foo),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )

View File

@@ -1,6 +1,3 @@
# E2502
print("\u202B\u202E\u05e9\u05DC\u05D5\u05DD\u202C")
# E2502
print("שלום‬")
@@ -20,5 +17,12 @@ def subtract_funds(account: str, amount: int):
return
# OK
print("\u202B\u202E\u05e9\u05DC\u05D5\u05DD\u202C")
# OK
print("\N{RIGHT-TO-LEFT MARK}")
# OK
print("Hello World")

View File

@@ -0,0 +1,17 @@
def a():
yield
def __init__():
yield
class A:
def __init__(self):
yield
class B:
def __init__(self):
yield from self.gen()
def gen(self):
yield 5

View File

@@ -1,6 +1,6 @@
use rustpython_parser::ast::Expr;
use crate::ast::helpers::to_call_path;
use crate::ast::helpers::{map_callable, to_call_path};
use crate::ast::types::{Scope, ScopeKind};
use crate::checkers::ast::Checker;
@@ -29,18 +29,20 @@ pub fn classify(
if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
checker.resolve_call_path(expr).map_or(false, |call_path| {
staticmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))
})
checker
.resolve_call_path(map_callable(expr))
.map_or(false, |call_path| {
staticmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))
})
}) {
FunctionType::StaticMethod
} else if CLASS_METHODS.contains(&name)
// Special-case class method, like `__new__`.
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
checker.resolve_call_path(expr).map_or(false, |call_path| {
checker.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
METACLASS_BASES
.iter()
.any(|(module, member)| call_path.as_slice() == [*module, *member])
@@ -48,7 +50,7 @@ pub fn classify(
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
checker.resolve_call_path(expr).map_or(false, |call_path| {
checker.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
classmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))

View File

@@ -568,6 +568,17 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
arg_names
}
/// Given an [`Expr`] that can be callable or not (like a decorator, which could
/// be used with or without explicit call syntax), return the underlying
/// callable.
pub fn map_callable(decorator: &Expr) -> &Expr {
if let ExprKind::Call { func, .. } = &decorator.node {
func
} else {
decorator
}
}
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
let start = if match_leading_content(located, locator) {

View File

@@ -6,8 +6,6 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
@@ -17,6 +15,9 @@ use rustpython_parser::ast::{
use rustpython_parser::parser;
use smallvec::smallvec;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use crate::ast::helpers::{
binding_range, collect_call_path, extract_handler_names, from_relative_import, to_module_path,
};
@@ -30,16 +31,15 @@ use crate::ast::typing::{match_annotated_subscript, Callable, SubscriptKind};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, typing, visitor};
use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable};
use crate::noqa::Directive;
use crate::registry::{Diagnostic, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return,
flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
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};
@@ -282,7 +282,7 @@ impl<'a> Checker<'a> {
}
/// Return `true` if a `Rule` is disabled by a `noqa` directive.
pub fn is_ignored(&self, code: &Rule, lineno: usize) -> bool {
pub fn rule_is_ignored(&self, code: &Rule, lineno: usize) -> bool {
// TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`.
// However, in rare cases, we need to check them here. For example, when
// removing unused imports, we create a single fix that's applied to all
@@ -293,16 +293,7 @@ impl<'a> Checker<'a> {
if matches!(self.noqa, flags::Noqa::Disabled) {
return false;
}
let noqa_lineno = self.noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = self.locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match noqa::extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => noqa::includes(code, &codes),
}
noqa::rule_is_ignored(code, lineno, self.noqa_line_for, self.locator)
}
}
@@ -1235,9 +1226,9 @@ where
}
}
if self.settings.rules.enabled(&Rule::ImportStarUsed) {
if self.settings.rules.enabled(&Rule::ImportStar) {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsed {
pyflakes::rules::ImportStar {
name: helpers::format_import_from(
level.as_ref(),
module.as_deref(),
@@ -1300,9 +1291,12 @@ where
if self.settings.rules.enabled(&Rule::RelativeImports) {
if let Some(diagnostic) =
flake8_tidy_imports::relative_imports::banned_relative_import(
self,
stmt,
level.as_ref(),
module.as_deref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
self.path,
)
{
self.diagnostics.push(diagnostic);
@@ -1537,7 +1531,7 @@ where
if self.settings.rules.enabled(&Rule::AssertTuple) {
pyflakes::rules::assert_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::DoNotAssertFalse) {
if self.settings.rules.enabled(&Rule::AssertFalse) {
flake8_bugbear::rules::assert_false(
self,
stmt,
@@ -1545,7 +1539,7 @@ where
msg.as_ref().map(|expr| &**expr),
);
}
if self.settings.rules.enabled(&Rule::AssertUsed) {
if self.settings.rules.enabled(&Rule::Assert) {
self.diagnostics
.push(flake8_bandit::rules::assert_used(stmt));
}
@@ -1704,9 +1698,9 @@ where
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let [target] = &targets[..] {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
@@ -1722,6 +1716,12 @@ where
}
}
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
if self.path.extension().map_or(false, |ext| ext == "pyi") {
flake8_pyi::rules::prefix_type_params(self, value, targets);
}
}
if self.settings.rules.enabled(&Rule::UselessMetaclassType) {
pyupgrade::rules::useless_metaclass_type(self, stmt, value, targets);
}
@@ -1754,9 +1754,9 @@ where
}
}
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let Some(value) = value {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
}
@@ -2372,7 +2372,7 @@ where
}
// flake8-bandit
if self.settings.rules.enabled(&Rule::ExecUsed) {
if self.settings.rules.enabled(&Rule::ExecBuiltin) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
self.diagnostics.push(diagnostic);
}
@@ -2405,6 +2405,9 @@ where
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
if self
.settings
.rules
@@ -2790,11 +2793,17 @@ where
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::YieldFrom { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::Await { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
@@ -2812,6 +2821,9 @@ where
{
pyflakes::rules::f_string_missing_placeholders(expr, values, self);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::BinOp {
left,
@@ -2973,6 +2985,12 @@ where
if self.settings.rules.enabled(&Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
}
if self.settings.rules.enabled(&Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(self, expr, right);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
}
ExprKind::BinOp {
@@ -2994,6 +3012,9 @@ where
{
ruff::rules::unpack_instead_of_concatenating_to_collection_literal(self, expr);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::UnaryOp { op, operand } => {
let check_not_in = self.settings.rules.enabled(&Rule::NotInTest);
@@ -3137,9 +3158,6 @@ where
if self.settings.rules.enabled(&Rule::RewriteUnicodeLiteral) {
pyupgrade::rules::rewrite_unicode_literal(self, expr, kind.as_deref());
}
if self.settings.rules.enabled(&Rule::BidirectionalUnicode) {
pylint::rules::bidirectional_unicode(self, expr, value);
}
}
ExprKind::Lambda { args, body, .. } => {
if self.settings.rules.enabled(&Rule::PreferListBuiltin) {
@@ -3521,8 +3539,8 @@ where
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.rules.enabled(&Rule::DoNotUseBareExcept) {
if let Some(diagnostic) = pycodestyle::rules::do_not_use_bare_except(
if self.settings.rules.enabled(&Rule::BareExcept) {
if let Some(diagnostic) = pycodestyle::rules::bare_except(
type_.as_deref(),
body,
excepthandler,
@@ -3549,6 +3567,17 @@ where
if self.settings.rules.enabled(&Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
self.settings.flake8_bandit.check_typed_exception,
);
}
if self.settings.rules.enabled(&Rule::TryExceptContinue) {
flake8_bandit::rules::try_except_continue(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
@@ -3855,6 +3884,14 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found"))]
}
pub fn current_scope_parent(&self) -> Option<&Scope> {
self.scope_stack
.iter()
.rev()
.nth(1)
.map(|index| &self.scopes[*index])
}
pub fn current_scopes(&self) -> impl Iterator<Item = &Scope> {
self.scope_stack
.iter()
@@ -4304,16 +4341,18 @@ impl<'a> Checker<'a> {
} {
let (all_names, all_names_flags) = extract_all_names(self, parent, current);
if self.settings.rules.enabled(&Rule::InvalidAllFormat)
&& matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT)
{
pylint::rules::invalid_all_format(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllFormat) {
if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) {
self.diagnostics
.push(pylint::rules::invalid_all_format(expr));
}
}
if self.settings.rules.enabled(&Rule::InvalidAllObject)
&& matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT)
{
pylint::rules::invalid_all_object(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllObject) {
if matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT) {
self.diagnostics
.push(pylint::rules::invalid_all_object(expr));
}
}
self.add_binding(
@@ -4578,11 +4617,54 @@ impl<'a> Checker<'a> {
return;
}
// Mark anything referenced in `__all__` as used.
let global_scope = &self.scopes[GLOBAL_SCOPE_INDEX];
let all_names: Option<(&Vec<String>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some((names, binding.range)),
_ => None,
});
let all_bindings: Option<(Vec<usize>, Range)> = all_names.map(|(names, range)| {
(
names
.iter()
.filter_map(|name| global_scope.bindings.get(name.as_str()).copied())
.collect(),
range,
)
});
if let Some((bindings, range)) = all_bindings {
for index in bindings {
self.bindings[index].mark_used(
GLOBAL_SCOPE_INDEX,
range,
ExecutionContext::Runtime,
);
}
}
// Extract `__all__` names from the global scope.
let all_names: Option<(Vec<&str>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some((names.iter().map(String::as_str).collect(), binding.range))
}
_ => None,
});
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let runtime_imports: Vec<Vec<&Binding>> = if !self.settings.flake8_type_checking.strict
&& (self
let runtime_imports: Vec<Vec<&Binding>> = if self.settings.flake8_type_checking.strict {
vec![]
} else {
if self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
@@ -4597,29 +4679,41 @@ impl<'a> Checker<'a> {
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
}
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for (index, stack) in self.dead_scopes.iter().rev() {
let scope = &self.scopes[*index];
// F822
if *index == GLOBAL_SCOPE_INDEX {
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if let Some((names, range)) = &all_names {
diagnostics.extend(pyflakes::rules::undefined_export(
names, range, self.path, scope,
));
}
}
}
// PLW0602
if self
.settings
@@ -4648,35 +4742,6 @@ impl<'a> Checker<'a> {
continue;
}
let all_binding: Option<&Binding> = scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index]);
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names.iter().map(String::as_str).collect()),
_ => None,
});
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedExport {
name: name.to_string(),
},
all_binding.range,
));
}
}
}
}
}
}
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `redefinitions` if
// the bindings are in different scopes.
@@ -4692,13 +4757,7 @@ impl<'a> Checker<'a> {
| BindingKind::StarImportation(..)
| BindingKind::FutureImportation
) {
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4728,31 +4787,27 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
if scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
let mut from_list = vec![];
for binding in
scope.bindings.values().map(|index| &self.bindings[*index])
{
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
if let Some((names, range)) = &all_names {
let mut from_list = vec![];
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
from_list.sort();
}
from_list.sort();
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
all_binding.range,
));
}
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
*range,
));
}
}
}
@@ -4820,7 +4875,7 @@ impl<'a> Checker<'a> {
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in &scope.bindings {
for index in scope.bindings.values() {
let binding = &self.bindings[*index];
let full_name = match &binding.kind {
@@ -4830,13 +4885,7 @@ impl<'a> Checker<'a> {
_ => continue,
};
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4853,9 +4902,9 @@ impl<'a> Checker<'a> {
None
};
if self.is_ignored(&Rule::UnusedImport, diagnostic_lineno)
if self.rule_is_ignored(&Rule::UnusedImport, diagnostic_lineno)
|| parent_lineno.map_or(false, |parent_lineno| {
self.is_ignored(&Rule::UnusedImport, parent_lineno)
self.rule_is_ignored(&Rule::UnusedImport, parent_lineno)
})
{
ignored
@@ -4987,10 +5036,7 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::MissingReturnTypeClassMethod)
|| self
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression);
|| self.settings.rules.enabled(&Rule::AnyType);
let enforce_docstrings = self.settings.rules.enabled(&Rule::PublicModule)
|| self.settings.rules.enabled(&Rule::PublicClass)
|| self.settings.rules.enabled(&Rule::PublicMethod)
@@ -5030,11 +5076,11 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::SectionUnderlineNotOverIndented)
|| self.settings.rules.enabled(&Rule::UsesTripleQuotes)
|| self.settings.rules.enabled(&Rule::TripleSingleQuotes)
|| self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|| self.settings.rules.enabled(&Rule::NoSignature)
@@ -5178,13 +5224,13 @@ impl<'a> Checker<'a> {
{
pydocstyle::rules::multi_line_summary_start(self, &docstring);
}
if self.settings.rules.enabled(&Rule::UsesTripleQuotes) {
if self.settings.rules.enabled(&Rule::TripleSingleQuotes) {
pydocstyle::rules::triple_quotes(self, &docstring);
}
if self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
{
pydocstyle::rules::backslashes(self, &docstring);
}

View File

@@ -11,6 +11,7 @@ use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
};
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::rules::pylint;
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
use crate::source_code::Stylist;
@@ -41,6 +42,7 @@ pub fn check_physical_lines(
.rules
.enabled(&Rule::PEP3120UnnecessaryCodingComment);
let enforce_mixed_spaces_and_tabs = settings.rules.enabled(&Rule::MixedSpacesAndTabs);
let enforce_bidirectional_unicode = settings.rules.enabled(&Rule::BidirectionalUnicode);
let fix_unnecessary_coding_comment = matches!(autofix, flags::Autofix::Enabled)
&& settings
@@ -137,6 +139,10 @@ pub fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
if enforce_bidirectional_unicode {
diagnostics.extend(pylint::rules::bidirectional_unicode(index, line));
}
}
if enforce_no_newline_at_end_of_file {

View File

@@ -32,8 +32,18 @@ pub fn check_tokens(
let enforce_quotes = settings.rules.enabled(&Rule::BadQuotesInlineString)
|| settings.rules.enabled(&Rule::BadQuotesMultilineString)
|| settings.rules.enabled(&Rule::BadQuotesDocstring)
|| settings.rules.enabled(&Rule::AvoidQuoteEscape);
|| settings.rules.enabled(&Rule::AvoidableEscapedQuote);
let enforce_commented_out_code = settings.rules.enabled(&Rule::CommentedOutCode);
let enforce_compound_statements = settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineColon)
|| settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineSemicolon)
|| settings.rules.enabled(&Rule::UselessSemicolon)
|| settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineDef);
let enforce_invalid_escape_sequence = settings.rules.enabled(&Rule::InvalidEscapeSequence);
let enforce_implicit_string_concatenation = settings
.rules
@@ -48,10 +58,8 @@ pub fn check_tokens(
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
if enforce_ambiguous_unicode_character
|| enforce_commented_out_code
|| enforce_invalid_escape_sequence
{
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character {
@@ -60,54 +68,64 @@ pub fn check_tokens(
false
};
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
} else {
Context::String
}
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
} else {
Context::Comment
},
settings,
autofix,
));
}
Context::String
}
} else {
Context::Comment
},
settings,
autofix,
));
}
}
}
// eradicate
if enforce_commented_out_code {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
{
diagnostics.push(diagnostic);
}
}
}
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
start,
end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
// ERA001
if enforce_commented_out_code {
for (start, tok, end) in tokens.iter().flatten() {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, *start, *end, settings, autofix)
{
diagnostics.push(diagnostic);
}
}
}
}
// W605
if enforce_invalid_escape_sequence {
for (start, tok, end) in tokens.iter().flatten() {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
*start,
*end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
}
}
}
// E701, E702, E703, E704
if enforce_compound_statements {
diagnostics.extend(
pycodestyle::rules::compound_statements(tokens)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}
// Q001, Q002, Q003
if enforce_quotes {
diagnostics.extend(

View File

@@ -282,6 +282,10 @@ mod tests {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::F841.into(),
},
PatternPrefixPair {
pattern: "*.pyi".to_string(),
prefix: RuleCodePrefix::E704.into(),
},
];
assert_eq!(actual, expected);

View File

@@ -6,9 +6,10 @@
use rustpython_parser::lexer::Tok;
#[derive(Debug)]
#[derive(Default)]
enum State {
// Start of the module: first string gets marked as a docstring.
#[default]
ExpectModuleDocstring,
// After seeing a class definition, we're waiting for the block colon (and do bracket
// counting).
@@ -23,25 +24,13 @@ enum State {
Other,
}
#[derive(Default)]
pub struct StateMachine {
state: State,
bracket_count: usize,
}
impl Default for StateMachine {
fn default() -> Self {
Self::new()
}
}
impl StateMachine {
pub const fn new() -> Self {
Self {
state: State::ExpectModuleDocstring,
bracket_count: 0,
}
}
pub fn consume(&mut self, tok: &Tok) -> bool {
if matches!(
tok,

View File

@@ -5,6 +5,11 @@
//!
//! [Ruff]: https://github.com/charliermarsh/ruff
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
mod assert_yaml_snapshot;
mod ast;
mod autofix;
@@ -34,20 +39,12 @@ mod vendor;
mod violation;
mod visibility;
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
cfg_if! {
if #[cfg(not(target_family = "wasm"))] {
pub mod packaging;
mod lib_native;
pub use lib_native::check;
} else {
if #[cfg(target_family = "wasm")] {
mod lib_wasm;
pub use lib_wasm::check;
} else {
pub mod packaging;
}
}

View File

@@ -1,67 +0,0 @@
use std::path::Path;
use anyhow::Result;
use path_absolutize::path_dedot;
use rustpython_parser::lexer::LexResult;
use crate::linter::check_path;
use crate::registry::Diagnostic;
use crate::resolver::Relativity;
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::{flags, pyproject, Settings};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::{directives, packaging, resolver};
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_settings_toml(path)? {
// First priority: `pyproject.toml` in the current `Path`.
Ok(resolver::resolve_settings(&pyproject, &Relativity::Parent)?.lib)
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Second priority: user-specific `pyproject.toml`.
Ok(resolver::resolve_settings(&pyproject, &Relativity::Cwd)?.lib)
} else {
// Fallback: default settings.
Settings::from_configuration(Configuration::default(), &path_dedot::CWD)
}
}
/// Run Ruff over Python source code directly.
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnostic>> {
// Load the relevant `Settings` for the given `Path`.
let settings = resolve(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(&settings));
// Generate diagnostics.
let result = check_path(
path,
packaging::detect_package_root(path, &settings.namespace_packages),
contents,
tokens,
&locator,
&stylist,
&indexer,
&directives,
&settings,
autofix.into(),
flags::Noqa::Enabled,
);
Ok(result.data)
}

View File

@@ -18,7 +18,7 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::message::{Message, Source};
use crate::noqa::add_noqa;
use crate::noqa::{add_noqa, rule_is_ignored};
use crate::registry::{Diagnostic, LintSource, Rule};
use crate::rules::pycodestyle;
use crate::settings::{flags, Settings};
@@ -149,7 +149,17 @@ pub fn check_path(
if settings.rules.enabled(&Rule::SyntaxError) {
pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error);
}
error = Some(parse_error);
// If the syntax error is ignored, suppress it (regardless of whether
// `Rule::SyntaxError` is enabled).
if !rule_is_ignored(
&Rule::SyntaxError,
parse_error.location.row(),
&directives.noqa_line_for,
locator,
) {
error = Some(parse_error);
}
}
}
}

View File

@@ -7,10 +7,12 @@ use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::source_code::LineEnding;
use crate::source_code::{LineEnding, Locator};
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -76,6 +78,25 @@ pub fn includes(needle: &Rule, haystack: &[&str]) -> bool {
.any(|candidate| needle == get_redirect_target(candidate).unwrap_or(candidate))
}
/// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`.
pub fn rule_is_ignored(
code: &Rule,
lineno: usize,
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
) -> bool {
let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => includes(code, &codes),
}
}
pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],

View File

@@ -60,13 +60,17 @@ ruff_macros::define_rule_mapping!(
E401 => rules::pycodestyle::rules::MultipleImportsOnOneLine,
E402 => rules::pycodestyle::rules::ModuleImportNotAtTopOfFile,
E501 => rules::pycodestyle::rules::LineTooLong,
E701 => rules::pycodestyle::rules::MultipleStatementsOnOneLineColon,
E702 => rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon,
E703 => rules::pycodestyle::rules::UselessSemicolon,
E704 => rules::pycodestyle::rules::MultipleStatementsOnOneLineDef,
E711 => rules::pycodestyle::rules::NoneComparison,
E712 => rules::pycodestyle::rules::TrueFalseComparison,
E713 => rules::pycodestyle::rules::NotInTest,
E714 => rules::pycodestyle::rules::NotIsTest,
E721 => rules::pycodestyle::rules::TypeComparison,
E722 => rules::pycodestyle::rules::DoNotUseBareExcept,
E731 => rules::pycodestyle::rules::DoNotAssignLambda,
E722 => rules::pycodestyle::rules::BareExcept,
E731 => rules::pycodestyle::rules::LambdaAssignment,
E741 => rules::pycodestyle::rules::AmbiguousVariableName,
E742 => rules::pycodestyle::rules::AmbiguousClassName,
E743 => rules::pycodestyle::rules::AmbiguousFunctionName,
@@ -79,7 +83,7 @@ ruff_macros::define_rule_mapping!(
// pyflakes
F401 => rules::pyflakes::rules::UnusedImport,
F402 => rules::pyflakes::rules::ImportShadowedByLoopVar,
F403 => rules::pyflakes::rules::ImportStarUsed,
F403 => rules::pyflakes::rules::ImportStar,
F404 => rules::pyflakes::rules::LateFutureImport,
F405 => rules::pyflakes::rules::ImportStarUsage,
F406 => rules::pyflakes::rules::ImportStarNotPermitted,
@@ -121,8 +125,10 @@ ruff_macros::define_rule_mapping!(
F842 => rules::pyflakes::rules::UnusedAnnotation,
F901 => rules::pyflakes::rules::RaiseNotImplemented,
// pylint
PLE0100 => rules::pylint::rules::YieldInInit,
PLE0604 => rules::pylint::rules::InvalidAllObject,
PLE0605 => rules::pylint::rules::InvalidAllFormat,
PLE1307 => rules::pylint::rules::BadStringFormatType,
PLE2502 => rules::pylint::rules::BidirectionalUnicode,
PLE1310 => rules::pylint::rules::BadStrStripCall,
PLC0414 => rules::pylint::rules::UselessImportAlias,
@@ -156,7 +162,7 @@ ruff_macros::define_rule_mapping!(
B008 => rules::flake8_bugbear::rules::FunctionCallArgumentDefault,
B009 => rules::flake8_bugbear::rules::GetAttrWithConstant,
B010 => rules::flake8_bugbear::rules::SetAttrWithConstant,
B011 => rules::flake8_bugbear::rules::DoNotAssertFalse,
B011 => rules::flake8_bugbear::rules::AssertFalse,
B012 => rules::flake8_bugbear::rules::JumpStatementInFinally,
B013 => rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler,
B014 => rules::flake8_bugbear::rules::DuplicateHandlerException,
@@ -221,7 +227,7 @@ ruff_macros::define_rule_mapping!(
Q000 => rules::flake8_quotes::rules::BadQuotesInlineString,
Q001 => rules::flake8_quotes::rules::BadQuotesMultilineString,
Q002 => rules::flake8_quotes::rules::BadQuotesDocstring,
Q003 => rules::flake8_quotes::rules::AvoidQuoteEscape,
Q003 => rules::flake8_quotes::rules::AvoidableEscapedQuote,
// flake8-annotations
ANN001 => rules::flake8_annotations::rules::MissingTypeFunctionArgument,
ANN002 => rules::flake8_annotations::rules::MissingTypeArgs,
@@ -233,7 +239,7 @@ ruff_macros::define_rule_mapping!(
ANN204 => rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod,
ANN205 => rules::flake8_annotations::rules::MissingReturnTypeStaticMethod,
ANN206 => rules::flake8_annotations::rules::MissingReturnTypeClassMethod,
ANN401 => rules::flake8_annotations::rules::DynamicallyTypedExpression,
ANN401 => rules::flake8_annotations::rules::AnyType,
// flake8-2020
YTT101 => rules::flake8_2020::rules::SysVersionSlice3Referenced,
YTT102 => rules::flake8_2020::rules::SysVersion2Referenced,
@@ -332,8 +338,8 @@ ruff_macros::define_rule_mapping!(
D213 => rules::pydocstyle::rules::MultiLineSummarySecondLine,
D214 => rules::pydocstyle::rules::SectionNotOverIndented,
D215 => rules::pydocstyle::rules::SectionUnderlineNotOverIndented,
D300 => rules::pydocstyle::rules::UsesTripleQuotes,
D301 => rules::pydocstyle::rules::UsesRPrefixForBackslashedContent,
D300 => rules::pydocstyle::rules::TripleSingleQuotes,
D301 => rules::pydocstyle::rules::EscapeSequenceInDocstring,
D400 => rules::pydocstyle::rules::EndsInPeriod,
D401 => rules::pydocstyle::rules::NonImperativeMood,
D402 => rules::pydocstyle::rules::NoSignature,
@@ -376,15 +382,17 @@ ruff_macros::define_rule_mapping!(
// eradicate
ERA001 => rules::eradicate::rules::CommentedOutCode,
// flake8-bandit
S101 => rules::flake8_bandit::rules::AssertUsed,
S102 => rules::flake8_bandit::rules::ExecUsed,
S101 => rules::flake8_bandit::rules::Assert,
S102 => rules::flake8_bandit::rules::ExecBuiltin,
S103 => rules::flake8_bandit::rules::BadFilePermissions,
S104 => rules::flake8_bandit::rules::HardcodedBindAllInterfaces,
S105 => rules::flake8_bandit::rules::HardcodedPasswordString,
S106 => rules::flake8_bandit::rules::HardcodedPasswordFuncArg,
S107 => rules::flake8_bandit::rules::HardcodedPasswordDefault,
S608 => rules::flake8_bandit::rules::HardcodedSQLExpression,
S108 => rules::flake8_bandit::rules::HardcodedTempFile,
S110 => rules::flake8_bandit::rules::TryExceptPass,
S112 => rules::flake8_bandit::rules::TryExceptContinue,
S113 => rules::flake8_bandit::rules::RequestWithoutTimeout,
S324 => rules::flake8_bandit::rules::HashlibInsecureHashFunction,
S501 => rules::flake8_bandit::rules::RequestWithNoCertValidation,
@@ -437,6 +445,8 @@ ruff_macros::define_rule_mapping!(
EM101 => rules::flake8_errmsg::rules::RawStringInException,
EM102 => rules::flake8_errmsg::rules::FStringInException,
EM103 => rules::flake8_errmsg::rules::DotFormatInException,
// flake8-pyi
PYI001 => rules::flake8_pyi::rules::PrefixTypeParams,
// flake8-pytest-style
PT001 => rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle,
PT002 => rules::flake8_pytest_style::rules::FixturePositionalArgs,
@@ -627,6 +637,9 @@ pub enum Linter {
/// [flake8-print](https://pypi.org/project/flake8-print/)
#[prefix = "T20"]
Flake8Print,
/// [flake8-pyi](https://pypi.org/project/flake8-pyi/)
#[prefix = "PYI"]
Flake8Pyi,
/// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
#[prefix = "PT"]
Flake8PytestStyle,
@@ -742,12 +755,13 @@ impl Rule {
| Rule::ShebangMissingExecutableFile
| Rule::ShebangNotExecutable
| Rule::ShebangNewline
| Rule::BidirectionalUnicode
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
| Rule::AvoidQuoteEscape
| Rule::AvoidableEscapedQuote
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
@@ -758,6 +772,10 @@ impl Rule {
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaMissing
| Rule::TrailingCommaOnBareTupleProhibited
| Rule::MultipleStatementsOnOneLineColon
| Rule::UselessSemicolon
| Rule::MultipleStatementsOnOneLineDef
| Rule::MultipleStatementsOnOneLineSemicolon
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
@@ -843,6 +861,28 @@ mod tests {
use super::{Linter, Rule, RuleNamespace};
#[test]
fn test_rule_naming_convention() {
// The disallowed rule names are defined in a separate file so that they can also be picked up by add_rule.py.
let patterns: Vec<_> = include_str!("../resources/test/disallowed_rule_names.txt")
.trim()
.split('\n')
.map(|line| {
glob::Pattern::new(line).expect("malformed pattern in disallowed_rule_names.txt")
})
.collect();
for rule in Rule::iter() {
let rule_name = rule.as_ref();
for pattern in &patterns {
assert!(
!pattern.matches(rule_name),
"{rule_name} does not match naming convention, see CONTRIBUTING.md"
);
}
}
}
#[test]
fn check_code_serialization() {
for rule in Rule::iter() {

View File

@@ -10,14 +10,14 @@ use crate::source_code::Locator;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for commented-out Python code.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Commented-out code is dead code, and is often included inadvertently.
/// It should be removed.
///
/// ### Example
/// ## Example
/// ```python
/// # print('foo')
/// ```

View File

@@ -31,7 +31,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -59,7 +59,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -131,7 +131,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -148,7 +148,7 @@ mod tests {
allow_star_arg_any: true,
..Default::default()
},
..Settings::for_rules(vec![Rule::DynamicallyTypedExpression])
..Settings::for_rules(vec![Rule::AnyType])
},
)?;
assert_yaml_snapshot!(diagnostics);

View File

@@ -16,6 +16,25 @@ use crate::visibility;
use crate::visibility::Visibility;
define_violation!(
/// ## What it does
/// Checks that function arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(x):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(x: int):
/// ...
/// ```
pub struct MissingTypeFunctionArgument {
pub name: String,
}
@@ -29,6 +48,25 @@ impl Violation for MissingTypeFunctionArgument {
}
define_violation!(
/// ## What it does
/// Checks that function `*args` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(*args):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(*args: int):
/// ...
/// ```
pub struct MissingTypeArgs {
pub name: String,
}
@@ -42,6 +80,25 @@ impl Violation for MissingTypeArgs {
}
define_violation!(
/// ## What it does
/// Checks that function `**kwargs` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(**kwargs):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(**kwargs: int):
/// ...
/// ```
pub struct MissingTypeKwargs {
pub name: String,
}
@@ -55,6 +112,30 @@ impl Violation for MissingTypeKwargs {
}
define_violation!(
/// ## What it does
/// Checks that instance method `self` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// Note that many type checkers will infer the type of `self` automatically, so this
/// annotation is not strictly necessary.
///
/// ## Example
/// ```python
/// class Foo:
/// def bar(self):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def bar(self: "Foo"):
/// ...
/// ```
pub struct MissingTypeSelf {
pub name: String,
}
@@ -68,6 +149,32 @@ impl Violation for MissingTypeSelf {
}
define_violation!(
/// ## What it does
/// Checks that class method `cls` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// Note that many type checkers will infer the type of `cls` automatically, so this
/// annotation is not strictly necessary.
///
/// ## Example
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls: Type["Foo"]):
/// ...
/// ```
pub struct MissingTypeCls {
pub name: String,
}
@@ -81,6 +188,25 @@ impl Violation for MissingTypeCls {
}
define_violation!(
/// ## What it does
/// Checks that public functions and methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// def add(a, b):
/// return a + b
/// ```
///
/// Use instead:
/// ```python
/// def add(a: int, b: int) -> int:
/// return a + b
/// ```
pub struct MissingReturnTypePublicFunction {
pub name: String,
}
@@ -94,6 +220,25 @@ impl Violation for MissingReturnTypePublicFunction {
}
define_violation!(
/// ## What it does
/// Checks that private functions and methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// def _add(a, b):
/// return a + b
/// ```
///
/// Use instead:
/// ```python
/// def _add(a: int, b: int) -> int:
/// return a + b
/// ```
pub struct MissingReturnTypePrivateFunction {
pub name: String,
}
@@ -107,6 +252,38 @@ impl Violation for MissingReturnTypePrivateFunction {
}
define_violation!(
/// ## What it does
/// Checks that "special" methods, like `__init__`, `__new__`, and `__call__`, have
/// return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// Note that type checkers often allow you to omit the return type annotation for
/// `__init__` methods, as long as at least one argument has a type annotation. To
/// opt-in to this behavior, use the `mypy-init-return` setting in your `pyproject.toml`
/// or `ruff.toml` file:
///
/// ```toml
/// [tool.ruff.flake8-annotations]
/// mypy-init-return = true
/// ```
///
/// ## Example
/// ```python
/// class Foo:
/// def __init__(self, x: int):
/// self.x = x
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def __init__(self, x: int) -> None:
/// self.x = x
/// ```
pub struct MissingReturnTypeSpecialMethod {
pub name: String,
}
@@ -124,6 +301,29 @@ impl AlwaysAutofixableViolation for MissingReturnTypeSpecialMethod {
}
define_violation!(
/// ## What it does
/// Checks that static methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// class Foo:
/// @staticmethod
/// def bar():
/// return 1
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @staticmethod
/// def bar() -> int:
/// return 1
/// ```
pub struct MissingReturnTypeStaticMethod {
pub name: String,
}
@@ -137,6 +337,29 @@ impl Violation for MissingReturnTypeStaticMethod {
}
define_violation!(
/// ## What it does
/// Checks that class methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls):
/// return 1
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls) -> int:
/// return 1
/// ```
pub struct MissingReturnTypeClassMethod {
pub name: String,
}
@@ -150,14 +373,42 @@ impl Violation for MissingReturnTypeClassMethod {
}
define_violation!(
pub struct DynamicallyTypedExpression {
/// ## What it does
/// Checks that an expression is annotated with a more specific type than
/// `Any`.
///
/// ## Why is this bad?
/// `Any` is a special type indicating an unconstrained type. When an
/// expression is annotated with type `Any`, type checkers will allow all
/// operations on it.
///
/// It's better to be explicit about the type of an expression, and to use
/// `Any` as an "escape hatch" only when it is really needed.
///
/// ## Example
/// ```python
/// def foo(x: Any):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(x: int):
/// ...
/// ```
///
/// ## References
/// * [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
/// * [`typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
/// * [Mypy: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
pub struct AnyType {
pub name: String,
}
);
impl Violation for DynamicallyTypedExpression {
impl Violation for AnyType {
#[derive_message_formats]
fn message(&self) -> String {
let DynamicallyTypedExpression { name } = self;
let AnyType { name } = self;
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
}
@@ -192,7 +443,7 @@ fn check_dynamically_typed<F>(
{
if checker.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
DynamicallyTypedExpression { name: func() },
AnyType { name: func() },
Range::from_located(annotation),
));
};
@@ -238,11 +489,7 @@ pub fn definition(
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(
checker,
annotation,
@@ -275,11 +522,7 @@ pub fn definition(
if let Some(expr) = &arg.node.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
@@ -310,11 +553,7 @@ pub fn definition(
if let Some(expr) = &arg.node.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
@@ -372,11 +611,7 @@ pub fn definition(
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
}
} else if !(

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_annotations/mod.rs
source: crates/ruff/src/rules/flake8_annotations/mod.rs
expression: diagnostics
---
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 10
@@ -14,7 +14,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 15
@@ -25,7 +25,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 40
@@ -36,7 +36,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo_method
location:
row: 44

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_annotations/mod.rs
source: crates/ruff/src/rules/flake8_annotations/mod.rs
expression: diagnostics
---
- kind:
@@ -91,7 +91,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 44
@@ -102,7 +102,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 49
@@ -113,7 +113,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*args"
location:
row: 54
@@ -124,7 +124,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**kwargs"
location:
row: 54
@@ -135,7 +135,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*args"
location:
row: 59
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**kwargs"
location:
row: 64
@@ -168,7 +168,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 78
@@ -179,7 +179,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 82
@@ -190,7 +190,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*params"
location:
row: 86
@@ -201,7 +201,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**options"
location:
row: 86
@@ -212,7 +212,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*params"
location:
row: 90
@@ -223,7 +223,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**options"
location:
row: 94

View File

@@ -1,5 +1,7 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind};
use crate::checkers::ast::Checker;
const PASSWORD_NAMES: [&str; 7] = [
"password", "pass", "passwd", "pwd", "secret", "token", "secrete",
];
@@ -20,3 +22,21 @@ pub fn matches_password_name(string: &str) -> bool {
.iter()
.any(|name| string.to_lowercase().contains(name))
}
pub fn is_untyped_exception(type_: Option<&Expr>, checker: &Checker) -> bool {
type_.map_or(true, |type_| {
if let ExprKind::Tuple { elts, .. } = &type_.node {
elts.iter().any(|type_| {
checker.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
})
} else {
checker.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
}
})
}

View File

@@ -15,13 +15,14 @@ mod tests {
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::AssertUsed, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecUsed, Path::new("S102.py"); "S102")]
#[test_case(Rule::Assert, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecBuiltin, Path::new("S102.py"); "S102")]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"); "S103")]
#[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"); "S104")]
#[test_case(Rule::HardcodedPasswordString, Path::new("S105.py"); "S105")]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"); "S106")]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"); "S107")]
#[test_case(Rule::HardcodedSQLExpression, Path::new("S608.py"); "S608")]
#[test_case(Rule::HardcodedTempFile, Path::new("S108.py"); "S108")]
#[test_case(Rule::RequestWithoutTimeout, Path::new("S113.py"); "S113")]
#[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"); "S324")]
@@ -32,6 +33,7 @@ mod tests {
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"); "S110")]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -6,9 +6,9 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct AssertUsed;
pub struct Assert;
);
impl Violation for AssertUsed {
impl Violation for Assert {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `assert` detected")
@@ -18,7 +18,7 @@ impl Violation for AssertUsed {
/// S101
pub fn assert_used(stmt: &Located<StmtKind>) -> Diagnostic {
Diagnostic::new(
AssertUsed,
Assert,
Range::new(stmt.location, stmt.location.with_col_offset("assert".len())),
)
}

View File

@@ -6,9 +6,9 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct ExecUsed;
pub struct ExecBuiltin;
);
impl Violation for ExecUsed {
impl Violation for ExecBuiltin {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `exec` detected")
@@ -23,5 +23,5 @@ pub fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> {
if id != "exec" {
return None;
}
Some(Diagnostic::new(ExecUsed, Range::from_located(expr)))
Some(Diagnostic::new(ExecBuiltin, Range::from_located(expr)))
}

View File

@@ -0,0 +1,111 @@
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind, Operator};
use super::super::helpers::string_literal;
use crate::ast::helpers::{any_over_expr, unparse_expr};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
static SQL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(select\s.*from\s|delete\s+from\s|insert\s+into\s.*values\s|update\s.*set\s)")
.unwrap()
});
define_violation!(
/// ## What it does
/// Checks for strings that resemble SQL statements involved in some form
/// string building operation.
///
/// ## Why is this bad?
/// SQL injection is a common attack vector for web applications. Directly
/// interpolating user input into SQL statements should always be avoided.
/// Instead, favor parameterized queries, in which the SQL statement is
/// provided separately from its parameters, as supported by `psycopg3`
/// and other database drivers and ORMs.
///
/// ## Example
/// ```python
/// query = "DELETE FROM foo WHERE id = '%s'" % identifier
/// ```
///
/// ## References
/// * [B608: Test for SQL injection](https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html)
/// * [psycopg3: Server-side binding](https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#server-side-binding)
pub struct HardcodedSQLExpression {
pub string: String,
}
);
impl Violation for HardcodedSQLExpression {
#[derive_message_formats]
fn message(&self) -> String {
let HardcodedSQLExpression { string } = self;
format!(
"Possible SQL injection vector through string-based query construction: \"{}\"",
string.escape_debug()
)
}
}
fn has_string_literal(expr: &Expr) -> bool {
string_literal(expr).is_some()
}
fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Option<String> {
match &expr.node {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
ExprKind::BinOp {
op: Operator::Add | Operator::Mod,
..
} => {
let Some(parent) = checker.current_expr_parent() else {
if any_over_expr(expr, &has_string_literal) {
return Some(unparse_expr(expr, checker.stylist));
}
return None;
};
// Only evaluate the full BinOp, not the nested components.
let ExprKind::BinOp { .. } = &parent.node else {
if any_over_expr(expr, &has_string_literal) {
return Some(unparse_expr(expr, checker.stylist));
}
return None;
};
None
}
ExprKind::Call { func, .. } => {
let ExprKind::Attribute{ attr, value, .. } = &func.node else {
return None;
};
// "select * from table where val = {}".format(...)
if attr == "format" && string_literal(value).is_some() {
return Some(unparse_expr(expr, checker.stylist));
};
None
}
// f"select * from table where val = {val}"
ExprKind::JoinedStr { .. } => Some(unparse_expr(expr, checker.stylist)),
_ => None,
}
}
/// S608
pub fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
match unparse_string_format_expression(checker, expr) {
Some(string) if matches_sql_statement(&string) => {
checker.diagnostics.push(Diagnostic::new(
HardcodedSQLExpression { string },
Range::from_located(expr),
));
}
_ => (),
}
}

View File

@@ -1,6 +1,6 @@
pub use assert_used::{assert_used, AssertUsed};
pub use assert_used::{assert_used, Assert};
pub use bad_file_permissions::{bad_file_permissions, BadFilePermissions};
pub use exec_used::{exec_used, ExecUsed};
pub use exec_used::{exec_used, ExecBuiltin};
pub use hardcoded_bind_all_interfaces::{
hardcoded_bind_all_interfaces, HardcodedBindAllInterfaces,
};
@@ -9,6 +9,7 @@ pub use hardcoded_password_func_arg::{hardcoded_password_func_arg, HardcodedPass
pub use hardcoded_password_string::{
assign_hardcoded_password_string, compare_to_hardcoded_password_string, HardcodedPasswordString,
};
pub use hardcoded_sql_expression::{hardcoded_sql_expression, HardcodedSQLExpression};
pub use hardcoded_tmp_directory::{hardcoded_tmp_directory, HardcodedTempFile};
pub use hashlib_insecure_hash_functions::{
hashlib_insecure_hash_functions, HashlibInsecureHashFunction,
@@ -23,6 +24,7 @@ pub use request_with_no_cert_validation::{
pub use request_without_timeout::{request_without_timeout, RequestWithoutTimeout};
pub use snmp_insecure_version::{snmp_insecure_version, SnmpInsecureVersion};
pub use snmp_weak_cryptography::{snmp_weak_cryptography, SnmpWeakCryptography};
pub use try_except_continue::{try_except_continue, TryExceptContinue};
pub use try_except_pass::{try_except_pass, TryExceptPass};
pub use unsafe_yaml_load::{unsafe_yaml_load, UnsafeYAMLLoad};
@@ -33,6 +35,7 @@ mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
mod hardcoded_password_func_arg;
mod hardcoded_password_string;
mod hardcoded_sql_expression;
mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod jinja2_autoescape_false;
@@ -41,5 +44,6 @@ mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod try_except_continue;
mod try_except_pass;
mod unsafe_yaml_load;

View File

@@ -0,0 +1,39 @@
use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::rules::flake8_bandit::helpers::is_untyped_exception;
use crate::violation::Violation;
define_violation!(
pub struct TryExceptContinue;
);
impl Violation for TryExceptContinue {
#[derive_message_formats]
fn message(&self) -> String {
format!("`try`-`except`-`continue` detected, consider logging the exception")
}
}
/// S112
pub fn try_except_continue(
checker: &mut Checker,
excepthandler: &Excepthandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if body.len() == 1
&& body[0].node == StmtKind::Continue
&& (check_typed_exception || is_untyped_exception(type_, checker))
{
checker.diagnostics.push(Diagnostic::new(
TryExceptContinue,
Range::from_located(excepthandler),
));
}
}

View File

@@ -1,9 +1,11 @@
use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::rules::flake8_bandit::helpers::is_untyped_exception;
use crate::violation::Violation;
define_violation!(
@@ -19,6 +21,7 @@ impl Violation for TryExceptPass {
/// S110
pub fn try_except_pass(
checker: &mut Checker,
excepthandler: &Excepthandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
@@ -26,17 +29,11 @@ pub fn try_except_pass(
) {
if body.len() == 1
&& body[0].node == StmtKind::Pass
&& (check_typed_exception
|| type_.map_or(true, |type_| {
checker.resolve_call_path(type_).map_or(true, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
}))
&& (check_typed_exception || is_untyped_exception(type_, checker))
{
checker.diagnostics.push(Diagnostic::new(
TryExceptPass,
Range::from_located(&body[0]),
Range::from_located(excepthandler),
));
}
}

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
AssertUsed: ~
Assert: ~
location:
row: 2
column: 0
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
AssertUsed: ~
Assert: ~
location:
row: 8
column: 4
@@ -23,7 +23,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
AssertUsed: ~
Assert: ~
location:
row: 11
column: 4

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
ExecUsed: ~
ExecBuiltin: ~
location:
row: 3
column: 4
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
ExecUsed: ~
ExecBuiltin: ~
location:
row: 5
column: 0

View File

@@ -1,12 +1,12 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptPass: ~
location:
row: 4
column: 4
row: 3
column: 0
end_location:
row: 4
column: 8
@@ -15,8 +15,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 9
column: 4
row: 8
column: 0
end_location:
row: 9
column: 8

View File

@@ -1,12 +1,12 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptPass: ~
location:
row: 4
column: 4
row: 3
column: 0
end_location:
row: 4
column: 8
@@ -15,8 +15,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 9
column: 4
row: 8
column: 0
end_location:
row: 9
column: 8
@@ -25,8 +25,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 14
column: 4
row: 13
column: 0
end_location:
row: 14
column: 8

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptContinue: ~
location:
row: 3
column: 0
end_location:
row: 4
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 8
column: 0
end_location:
row: 9
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 13
column: 0
end_location:
row: 14
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 18
column: 0
end_location:
row: 19
column: 12
fix: ~
parent: ~

View File

@@ -0,0 +1,500 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
HardcodedSQLExpression:
string: "\"SELECT %s FROM table\" % (var,)"
location:
row: 2
column: 9
end_location:
row: 2
column: 40
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT var FROM \" + table"
location:
row: 3
column: 9
end_location:
row: 3
column: 35
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT \" + val + \" FROM \" + table"
location:
row: 4
column: 9
end_location:
row: 4
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT {} FROM table;\".format(var)"
location:
row: 5
column: 9
end_location:
row: 5
column: 44
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT * FROM table WHERE var = {var}\""
location:
row: 6
column: 9
end_location:
row: 6
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE var = %s\" % (var,)"
location:
row: 8
column: 9
end_location:
row: 8
column: 52
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE VAR = \" + var"
location:
row: 9
column: 9
end_location:
row: 9
column: 47
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM \" + table + \"WHERE var = \" + var"
location:
row: 10
column: 9
end_location:
row: 10
column: 54
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE var = {}\".format(var)"
location:
row: 11
column: 9
end_location:
row: 11
column: 55
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"DELETE FROM table WHERE var = {var}\""
location:
row: 12
column: 10
end_location:
row: 12
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO table VALUES (%s)\" % (var,)"
location:
row: 14
column: 10
end_location:
row: 14
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO TABLE VALUES (\" + var + \")\""
location:
row: 15
column: 10
end_location:
row: 15
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO {} VALUES ({})\".format(table, var)"
location:
row: 16
column: 10
end_location:
row: 16
column: 57
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"INSERT INTO {table} VALUES var = {var}\""
location:
row: 17
column: 10
end_location:
row: 17
column: 51
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE %s SET var = %s\" % (table, var)"
location:
row: 19
column: 10
end_location:
row: 19
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE \" + table + \" SET var = \" + var"
location:
row: 20
column: 10
end_location:
row: 20
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE {} SET var = {}\".format(table, var)"
location:
row: 21
column: 10
end_location:
row: 21
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"UPDATE {table} SET var = {var}\""
location:
row: 22
column: 10
end_location:
row: 22
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select %s from table\" % (var,)"
location:
row: 24
column: 10
end_location:
row: 24
column: 41
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select var from \" + table"
location:
row: 25
column: 10
end_location:
row: 25
column: 36
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select \" + val + \" from \" + table"
location:
row: 26
column: 10
end_location:
row: 26
column: 44
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select {} from table;\".format(var)"
location:
row: 27
column: 10
end_location:
row: 27
column: 45
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"select * from table where var = {var}\""
location:
row: 28
column: 10
end_location:
row: 28
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = %s\" % (var,)"
location:
row: 30
column: 10
end_location:
row: 30
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = \" + var"
location:
row: 31
column: 10
end_location:
row: 31
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from \" + table + \"where var = \" + var"
location:
row: 32
column: 10
end_location:
row: 32
column: 55
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = {}\".format(var)"
location:
row: 33
column: 10
end_location:
row: 33
column: 56
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"delete from table where var = {var}\""
location:
row: 34
column: 10
end_location:
row: 34
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into table values (%s)\" % (var,)"
location:
row: 36
column: 10
end_location:
row: 36
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into table values (\" + var + \")\""
location:
row: 37
column: 10
end_location:
row: 37
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into {} values ({})\".format(table, var)"
location:
row: 38
column: 10
end_location:
row: 38
column: 57
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"insert into {table} values var = {var}\""
location:
row: 39
column: 10
end_location:
row: 39
column: 51
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update %s set var = %s\" % (table, var)"
location:
row: 41
column: 10
end_location:
row: 41
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update \" + table + \" set var = \" + var"
location:
row: 42
column: 10
end_location:
row: 42
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update {} set var = {}\".format(table, var)"
location:
row: 43
column: 10
end_location:
row: 43
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"update {table} set var = {var}\""
location:
row: 44
column: 10
end_location:
row: 44
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM table\\n WHERE var = %s\\n \" % var"
location:
row: 48
column: 11
end_location:
row: 52
column: 13
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM TABLE\\n WHERE var =\\n \" + var"
location:
row: 55
column: 11
end_location:
row: 59
column: 13
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM table\\n WHERE var = {}\\n \".format(var)"
location:
row: 62
column: 11
end_location:
row: 66
column: 19
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"\\n SELECT *\\n FROM table\\n WHERE var = {var}\\n \""
location:
row: 69
column: 11
end_location:
row: 73
column: 7
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT *FROM tableWHERE var = {var}\""
location:
row: 77
column: 8
end_location:
row: 79
column: 28
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = %s\" % var"
location:
row: 83
column: 25
end_location:
row: 83
column: 67
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT * FROM table WHERE var = {var}\""
location:
row: 84
column: 25
end_location:
row: 84
column: 65
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = {}\".format(var)"
location:
row: 85
column: 25
end_location:
row: 85
column: 73
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = %s\" % var"
location:
row: 86
column: 29
end_location:
row: 86
column: 71
fix: ~
parent: ~

View File

@@ -23,7 +23,7 @@ mod tests {
#[test_case(Rule::FunctionCallArgumentDefault, Path::new("B006_B008.py"); "B008")]
#[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"); "B009")]
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"); "B010")]
#[test_case(Rule::DoNotAssertFalse, Path::new("B011.py"); "B011")]
#[test_case(Rule::AssertFalse, Path::new("B011.py"); "B011")]
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"); "B012")]
#[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"); "B013")]
#[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"); "B014")]

View File

@@ -9,9 +9,9 @@ use crate::registry::Diagnostic;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
pub struct DoNotAssertFalse;
pub struct AssertFalse;
);
impl AlwaysAutofixableViolation for DoNotAssertFalse {
impl AlwaysAutofixableViolation for AssertFalse {
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`")
@@ -61,7 +61,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
return;
};
let mut diagnostic = Diagnostic::new(DoNotAssertFalse, Range::from_located(test));
let mut diagnostic = Diagnostic::new(AssertFalse, Range::from_located(test));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
unparse_stmt(&assertion_error(msg), checker.stylist),

View File

@@ -7,17 +7,17 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for `self.assertRaises(Exception)`.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// `assertRaises(Exception)` can lead to your test passing even if the
/// code being tested is never executed due to a typo.
///
/// Either assert for a more specific exception (builtin or custom), use
/// `assertRaisesRegex` or the context manager form of `assertRaises`.
///
/// ### Example
/// ## Example
/// ```python
/// self.assertRaises(Exception, foo)
/// ```

View File

@@ -2,7 +2,7 @@ pub use abstract_base_class::{
abstract_base_class, AbstractBaseClassWithoutAbstractMethod,
EmptyMethodWithoutAbstractDecorator,
};
pub use assert_false::{assert_false, DoNotAssertFalse};
pub use assert_false::{assert_false, AssertFalse};
pub use assert_raises_exception::{assert_raises_exception, AssertRaisesException};
pub use assignment_to_os_environ::{assignment_to_os_environ, AssignmentToOsEnviron};
pub use cached_instance_method::{cached_instance_method, CachedInstanceMethod};

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bugbear/mod.rs
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
expression: diagnostics
---
- kind:
DoNotAssertFalse: ~
AssertFalse: ~
location:
row: 8
column: 7
@@ -21,7 +21,7 @@ expression: diagnostics
column: 12
parent: ~
- kind:
DoNotAssertFalse: ~
AssertFalse: ~
location:
row: 10
column: 7

View File

@@ -7,11 +7,11 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for imports that are typically imported using a common convention,
/// like `import pandas as pd`, and enforces that convention.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Consistency is good. Use a common convention for imports to make your code
/// more readable and idiomatic.
///
@@ -19,7 +19,7 @@ define_violation!(
/// convention for importing the `pandas` library, and users typically expect
/// Pandas to be aliased as `pd`.
///
/// ### Example
/// ## Example
/// ```python
/// import pandas
/// ```

View File

@@ -8,29 +8,34 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for packages that are missing an `__init__.py` file.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Python packages are directories that contain a file named `__init__.py`.
/// The existence of this file indicates that the directory is a Python
/// package, and so it can be imported the same way a module can be
/// imported.
///
/// Directories that lack an `__init__.py` file can still be imported, but
/// they're indicative of a special kind of package, known as a namespace
/// package (see: [PEP 420](https://www.python.org/dev/peps/pep-0420/)).
/// they're indicative of a special kind of package, known as a "namespace
/// package" (see: [PEP 420](https://www.python.org/dev/peps/pep-0420/)).
/// Namespace packages are less widely used, so a package that lacks an
/// `__init__.py` file is typically meant to be a regular package, and
/// the absence of the `__init__.py` file is probably an oversight.
///
/// Namespace packages are a relatively new feature of Python, and they're
/// not widely used. So a package that lacks an `__init__.py` file is
/// typically meant to be a regular package, and the absence of the
/// `__init__.py` file is probably an oversight.
pub struct ImplicitNamespacePackage(pub String);
/// Note that namespace packages can be specified via the
/// [`namespace-packages`](https://github.com/charliermarsh/ruff#namespace-packages)
/// configuration option. Adding a namespace package to the configuration
/// will suppress this violation for a given package.
pub struct ImplicitNamespacePackage {
pub filename: String,
}
);
impl Violation for ImplicitNamespacePackage {
#[derive_message_formats]
fn message(&self) -> String {
let ImplicitNamespacePackage(filename) = self;
let ImplicitNamespacePackage { filename } = self;
format!("File `{filename}` is part of an implicit namespace package. Add an `__init__.py`.")
}
}
@@ -59,7 +64,9 @@ pub fn implicit_namespace_package(
.to_string_lossy()
.replace(std::path::MAIN_SEPARATOR, "/"); // The snapshot test expects / as the path separator.
Some(Diagnostic::new(
ImplicitNamespacePackage(fs::relativize_path(path)),
ImplicitNamespacePackage {
filename: fs::relativize_path(path),
},
Range::default(),
))
} else {

View File

@@ -3,7 +3,8 @@ source: crates/ruff/src/rules/flake8_no_pep420/mod.rs
expression: diagnostics
---
- kind:
ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_empty/example.py"
ImplicitNamespacePackage:
filename: "./resources/test/fixtures/flake8_no_pep420/test_fail_empty/example.py"
location:
row: 1
column: 0

View File

@@ -3,7 +3,8 @@ source: crates/ruff/src/rules/flake8_no_pep420/mod.rs
expression: diagnostics
---
- kind:
ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py"
ImplicitNamespacePackage:
filename: "./resources/test/fixtures/flake8_no_pep420/test_fail_nonempty/example.py"
location:
row: 1
column: 0

View File

@@ -3,7 +3,8 @@ source: crates/ruff/src/rules/flake8_no_pep420/mod.rs
expression: diagnostics
---
- kind:
ImplicitNamespacePackage: "./resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py"
ImplicitNamespacePackage:
filename: "./resources/test/fixtures/flake8_no_pep420/test_fail_shebang/example.py"
location:
row: 1
column: 0

View File

@@ -0,0 +1,26 @@
//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/).
pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::PrefixTypeParams, Path::new("PYI001.pyi"))]
#[test_case(Rule::PrefixTypeParams, Path::new("PYI001.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,92 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind};
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum VarKind {
TypeVar,
ParamSpec,
TypeVarTuple,
}
impl fmt::Display for VarKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
VarKind::TypeVar => fmt.write_str("TypeVar"),
VarKind::ParamSpec => fmt.write_str("ParamSpec"),
VarKind::TypeVarTuple => fmt.write_str("TypeVarTuple"),
}
}
}
define_violation!(
/// ## What it does
/// Checks that type `TypeVar`, `ParamSpec`, and `TypeVarTuple` definitions in
/// stubs are prefixed with `_`.
///
/// ## Why is this bad?
/// By prefixing type parameters with `_`, we can avoid accidentally exposing
/// names internal to the stub.
///
/// ## Example
/// ```python
/// from typing import TypeVar
///
/// T = TypeVar("T")
/// ```
///
/// Use instead:
/// ```python
/// from typing import TypeVar
///
/// _T = TypeVar("_T")
/// ```
pub struct PrefixTypeParams {
pub kind: VarKind,
}
);
impl Violation for PrefixTypeParams {
#[derive_message_formats]
fn message(&self) -> String {
let PrefixTypeParams { kind } = self;
format!("Name of private `{kind}` must start with _")
}
}
/// PYI001
pub fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
if targets.len() != 1 {
return;
}
if let ExprKind::Name { id, .. } = &targets[0].node {
if id.starts_with('_') {
return;
}
};
if let ExprKind::Call { func, .. } = &value.node {
let Some(kind) = checker.resolve_call_path(func).and_then(|call_path| {
if checker.match_typing_call_path(&call_path, "ParamSpec") {
Some(VarKind::ParamSpec)
} else if checker.match_typing_call_path(&call_path, "TypeVar") {
Some(VarKind::TypeVar)
} else if checker.match_typing_call_path(&call_path, "TypeVarTuple") {
Some(VarKind::TypeVarTuple)
} else {
None
}
}) else {
return;
};
checker.diagnostics.push(Diagnostic::new(
PrefixTypeParams { kind },
Range::from_located(value),
));
}
}

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,38 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
- kind:
PrefixTypeParams:
kind: TypeVar
location:
row: 3
column: 4
end_location:
row: 3
column: 16
fix: ~
parent: ~
- kind:
PrefixTypeParams:
kind: TypeVarTuple
location:
row: 5
column: 9
end_location:
row: 5
column: 31
fix: ~
parent: ~
- kind:
PrefixTypeParams:
kind: ParamSpec
location:
row: 7
column: 4
end_location:
row: 7
column: 18
fix: ~
parent: ~

View File

@@ -1,21 +1,11 @@
use num_traits::identities::Zero;
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use crate::ast::helpers::collect_call_path;
use crate::ast::helpers::{collect_call_path, map_callable};
use crate::checkers::ast::Checker;
const ITERABLE_INITIALIZERS: &[&str] = &["dict", "frozenset", "list", "tuple", "set"];
/// Given a decorators that can be used with or without explicit call syntax, return
/// the underlying callable.
fn callable_decorator(decorator: &Expr) -> &Expr {
if let ExprKind::Call { func, .. } = &decorator.node {
func
} else {
decorator
}
}
pub fn get_mark_decorators(decorators: &[Expr]) -> impl Iterator<Item = &Expr> {
decorators
.iter()
@@ -23,9 +13,7 @@ pub fn get_mark_decorators(decorators: &[Expr]) -> impl Iterator<Item = &Expr> {
}
pub fn get_mark_name(decorator: &Expr) -> &str {
collect_call_path(callable_decorator(decorator))
.last()
.unwrap()
collect_call_path(map_callable(decorator)).last().unwrap()
}
pub fn is_pytest_fail(call: &Expr, checker: &Checker) -> bool {
@@ -47,7 +35,7 @@ pub fn is_pytest_fixture(decorator: &Expr, checker: &Checker) -> bool {
}
pub fn is_pytest_mark(decorator: &Expr) -> bool {
let segments = collect_call_path(callable_decorator(decorator));
let segments = collect_call_path(map_callable(decorator));
if segments.len() > 2 {
segments[0] == "pytest" && segments[1] == "mark"
} else {
@@ -57,7 +45,7 @@ pub fn is_pytest_mark(decorator: &Expr) -> bool {
pub fn is_pytest_yield_fixture(decorator: &Expr, checker: &Checker) -> bool {
checker
.resolve_call_path(callable_decorator(decorator))
.resolve_call_path(map_callable(decorator))
.map_or(false, |call_path| {
call_path.as_slice() == ["pytest", "yield_fixture"]
})
@@ -115,7 +103,7 @@ pub fn is_falsy_constant(expr: &Expr) -> bool {
pub fn is_pytest_parametrize(decorator: &Expr, checker: &Checker) -> bool {
checker
.resolve_call_path(callable_decorator(decorator))
.resolve_call_path(map_callable(decorator))
.map_or(false, |call_path| {
call_path.as_slice() == ["pytest", "mark", "parametrize"]
})

View File

@@ -36,7 +36,7 @@ mod tests {
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidQuoteEscape,
Rule::AvoidableEscapedQuote,
])
},
)?;
@@ -65,7 +65,7 @@ mod tests {
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidQuoteEscape,
Rule::AvoidableEscapedQuote,
])
},
)?;
@@ -98,7 +98,7 @@ mod tests {
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidQuoteEscape,
Rule::AvoidableEscapedQuote,
])
},
)?;
@@ -131,7 +131,7 @@ mod tests {
Rule::BadQuotesInlineString,
Rule::BadQuotesMultilineString,
Rule::BadQuotesDocstring,
Rule::AvoidQuoteEscape,
Rule::AvoidableEscapedQuote,
])
},
)?;

View File

@@ -12,16 +12,16 @@ use crate::source_code::Locator;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for inline strings that use single quotes or double quotes,
/// depending on the value of the [`inline-quotes`](https://github.com/charliermarsh/ruff#inline-quotes)
/// setting.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for inline
/// strings, but be consistent.
///
/// ### Example
/// ## Example
/// ```python
/// foo = 'bar'
/// ```
@@ -54,16 +54,16 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString {
}
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for multiline strings that use single quotes or double quotes,
/// depending on the value of the [`multiline-quotes`](https://github.com/charliermarsh/ruff#multiline-quotes)
/// setting.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for multiline
/// strings, but be consistent.
///
/// ### Example
/// ## Example
/// ```python
/// foo = '''
/// bar
@@ -100,15 +100,15 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString {
}
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for docstrings that use single quotes or double quotes, depending on the value of the [`docstring-quotes`](https://github.com/charliermarsh/ruff#docstring-quotes)
/// setting.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for docstring
/// strings, but be consistent.
///
/// ### Example
/// ## Example
/// ```python
/// '''
/// bar
@@ -145,15 +145,15 @@ impl AlwaysAutofixableViolation for BadQuotesDocstring {
}
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for strings that include escaped quotes, and suggests changing
/// the quote style to avoid the need to escape them.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// It's preferable to avoid escaped quotes in strings. By changing the
/// outer quote style, you can avoid escaping inner quotes.
///
/// ### Example
/// ## Example
/// ```python
/// foo = 'bar\'s'
/// ```
@@ -162,9 +162,9 @@ define_violation!(
/// ```python
/// foo = "bar's"
/// ```
pub struct AvoidQuoteEscape;
pub struct AvoidableEscapedQuote;
);
impl AlwaysAutofixableViolation for AvoidQuoteEscape {
impl AlwaysAutofixableViolation for AvoidableEscapedQuote {
#[derive_message_formats]
fn message(&self) -> String {
format!("Change outer quotes to avoid escaping inner quotes")
@@ -379,9 +379,9 @@ fn strings(
&& !string_contents.contains(bad_single(&quotes_settings.inline_quotes))
{
let mut diagnostic =
Diagnostic::new(AvoidQuoteEscape, Range::new(*start, *end));
Diagnostic::new(AvoidableEscapedQuote, Range::new(*start, *end));
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::AvoidQuoteEscape)
&& settings.rules.should_fix(&Rule::AvoidableEscapedQuote)
{
let quote = bad_single(&quotes_settings.inline_quotes);

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_quotes/mod.rs
source: crates/ruff/src/rules/flake8_quotes/mod.rs
expression: diagnostics
---
- kind:
AvoidQuoteEscape: ~
AvoidableEscapedQuote: ~
location:
row: 1
column: 25
@@ -21,7 +21,7 @@ expression: diagnostics
column: 47
parent: ~
- kind:
AvoidQuoteEscape: ~
AvoidableEscapedQuote: ~
location:
row: 9
column: 4

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_quotes/mod.rs
source: crates/ruff/src/rules/flake8_quotes/mod.rs
expression: diagnostics
---
- kind:
AvoidQuoteEscape: ~
AvoidableEscapedQuote: ~
location:
row: 1
column: 25
@@ -21,7 +21,7 @@ expression: diagnostics
column: 47
parent: ~
- kind:
AvoidQuoteEscape: ~
AvoidableEscapedQuote: ~
location:
row: 2
column: 25
@@ -39,7 +39,7 @@ expression: diagnostics
column: 52
parent: ~
- kind:
AvoidQuoteEscape: ~
AvoidableEscapedQuote: ~
location:
row: 10
column: 4

View File

@@ -165,6 +165,36 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) {
}
}
const NORETURN_FUNCS: &[&[&str]] = &[
// builtins
&["", "exit"],
&["", "quit"],
// stdlib
&["builtins", "exit"],
&["builtins", "quit"],
&["os", "_exit"],
&["os", "abort"],
&["posix", "_exit"],
&["posix", "abort"],
&["sys", "exit"],
&["_thread", "exit"],
&["_winapi", "ExitProcess"],
// third-party modules
&["pytest", "exit"],
&["pytest", "fail"],
&["pytest", "skip"],
&["pytest", "xfail"],
];
/// Return `true` if the `func` is a known function that never returns.
fn is_noreturn_func(checker: &Checker, func: &Expr) -> bool {
checker.resolve_call_path(func).map_or(false, |call_path| {
NORETURN_FUNCS
.iter()
.any(|target| call_path.as_slice() == *target)
})
}
/// RET503
fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) {
match &last_stmt.node {
@@ -208,6 +238,12 @@ fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) {
| StmtKind::While { .. }
| StmtKind::Raise { .. }
| StmtKind::Try { .. } => {}
StmtKind::Expr { value, .. }
if matches!(
&value.node,
ExprKind::Call { func, .. }
if is_noreturn_func(checker, func)
) => {}
_ => {
let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from_located(last_stmt));
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -1,82 +1,120 @@
---
source: src/rules/flake8_return/mod.rs
source: crates/ruff/src/rules/flake8_return/mod.rs
expression: diagnostics
---
- kind:
ImplicitReturn: ~
location:
row: 7
row: 18
column: 4
end_location:
row: 8
row: 19
column: 16
fix: ~
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 14
row: 25
column: 8
end_location:
row: 14
row: 25
column: 15
fix:
content:
- " return None"
- ""
location:
row: 15
row: 26
column: 0
end_location:
row: 15
row: 26
column: 0
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 23
row: 34
column: 4
end_location:
row: 23
row: 34
column: 11
fix:
content:
- " return None"
- ""
location:
row: 24
row: 35
column: 0
end_location:
row: 24
row: 35
column: 0
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 29
row: 40
column: 8
end_location:
row: 30
row: 41
column: 20
fix: ~
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 39
row: 50
column: 8
end_location:
row: 39
row: 50
column: 15
fix:
content:
- " return None"
- ""
location:
row: 40
row: 51
column: 0
end_location:
row: 40
row: 51
column: 0
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 57
column: 4
end_location:
row: 57
column: 22
fix:
content:
- " return None"
- ""
location:
row: 58
column: 0
end_location:
row: 58
column: 0
parent: ~
- kind:
ImplicitReturn: ~
location:
row: 64
column: 4
end_location:
row: 64
column: 21
fix:
content:
- " return None"
- ""
location:
row: 65
column: 0
end_location:
row: 65
column: 0
parent: ~

View File

@@ -1,6 +1,8 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::helpers::collect_call_path;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
@@ -25,20 +27,17 @@ const VALID_IDS: [&str; 3] = ["self", "cls", "mcs"];
pub fn private_member_access(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if !attr.ends_with("__") && (attr.starts_with('_') || attr.starts_with("__")) {
let id = match &value.node {
ExprKind::Name { id, .. } => id,
ExprKind::Attribute { attr, .. } => attr,
_ => return,
};
if !VALID_IDS.contains(&id.as_str()) {
checker.diagnostics.push(Diagnostic::new(
PrivateMemberAccess {
access: format!("{}.{}", id, attr),
},
Range::from_located(expr),
));
let call_path = collect_call_path(value);
if VALID_IDS.iter().any(|id| call_path.as_slice() == [*id]) {
return;
}
checker.diagnostics.push(Diagnostic::new(
PrivateMemberAccess {
access: attr.to_string(),
},
Range::from_located(expr),
));
}
}
}

View File

@@ -1,10 +1,10 @@
---
source: src/rules/flake8_self/mod.rs
source: crates/ruff/src/rules/flake8_self/mod.rs
expression: diagnostics
---
- kind:
PrivateMemberAccess:
access: bar._private
access: _private
location:
row: 34
column: 11
@@ -15,57 +15,79 @@ expression: diagnostics
parent: ~
- kind:
PrivateMemberAccess:
access: foo._private_thing
access: _private
location:
row: 55
row: 36
column: 11
end_location:
row: 36
column: 30
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: _private_thing
location:
row: 58
column: 6
end_location:
row: 55
row: 58
column: 24
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: foo.__really_private_thing
access: __really_private_thing
location:
row: 56
row: 59
column: 6
end_location:
row: 56
row: 59
column: 32
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: foo._private_func
access: _private_func
location:
row: 57
row: 60
column: 6
end_location:
row: 57
row: 60
column: 23
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: foo.__really_private_func
access: __really_private_func
location:
row: 58
row: 61
column: 6
end_location:
row: 58
row: 61
column: 31
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: bar._private
access: _private
location:
row: 59
row: 62
column: 6
end_location:
row: 59
row: 62
column: 22
fix: ~
parent: ~
- kind:
PrivateMemberAccess:
access: _private_thing
location:
row: 63
column: 6
end_location:
row: 63
column: 26
fix: ~
parent: ~

View File

@@ -1,7 +1,8 @@
use log::error;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::comparable::ComparableExpr;
use crate::ast::helpers::{
contains_call_path, contains_effect, create_expr, create_stmt, first_colon_range, has_comments,
@@ -12,44 +13,62 @@ use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::rules::flake8_simplify::rules::fix_if;
use crate::violation::{AlwaysAutofixableViolation, Availability, Violation};
use crate::AutofixKind;
use crate::violation::{AutofixKind, Availability, Violation};
define_violation!(
pub struct NestedIfStatements;
pub struct NestedIfStatements {
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for NestedIfStatements {
impl Violation for NestedIfStatements {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
format!("Use a single `if` statement instead of nested `if` statements")
}
fn autofix_title(&self) -> String {
"Combine `if` statements using `and`".to_string()
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let NestedIfStatements { fixable, .. } = self;
if *fixable {
Some(|_| format!("Combine `if` statements using `and`"))
} else {
None
}
}
}
define_violation!(
pub struct ReturnBoolConditionDirectly {
pub cond: String,
pub condition: String,
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for ReturnBoolConditionDirectly {
impl Violation for ReturnBoolConditionDirectly {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let ReturnBoolConditionDirectly { cond } = self;
format!("Return the condition `{cond}` directly")
let ReturnBoolConditionDirectly { condition, .. } = self;
format!("Return the condition `{condition}` directly")
}
fn autofix_title(&self) -> String {
let ReturnBoolConditionDirectly { cond } = self;
format!("Replace with `return {cond}`")
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let ReturnBoolConditionDirectly { fixable, .. } = self;
if *fixable {
Some(|ReturnBoolConditionDirectly { condition, .. }| {
format!("Replace with `return {condition}`")
})
} else {
None
}
}
}
define_violation!(
pub struct UseTernaryOperator {
pub contents: String,
pub fixable: bool,
}
);
impl Violation for UseTernaryOperator {
@@ -57,30 +76,44 @@ impl Violation for UseTernaryOperator {
#[derive_message_formats]
fn message(&self) -> String {
let UseTernaryOperator { contents } = self;
let UseTernaryOperator { contents, .. } = self;
format!("Use ternary operator `{contents}` instead of if-else-block")
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
Some(|UseTernaryOperator { contents }| format!("Replace if-else-block with `{contents}`"))
let UseTernaryOperator { fixable, .. } = self;
if *fixable {
Some(|UseTernaryOperator { contents, .. }| {
format!("Replace if-else-block with `{contents}`")
})
} else {
None
}
}
}
define_violation!(
pub struct DictGetWithDefault {
pub contents: String,
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for DictGetWithDefault {
impl Violation for DictGetWithDefault {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let DictGetWithDefault { contents } = self;
let DictGetWithDefault { contents, .. } = self;
format!("Use `{contents}` instead of an `if` block")
}
fn autofix_title(&self) -> String {
let DictGetWithDefault { contents } = self;
format!("Replace with `{contents}`")
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let DictGetWithDefault { fixable, .. } = self;
if *fixable {
Some(|DictGetWithDefault { contents, .. }| format!("Replace with `{contents}`"))
} else {
None
}
}
}
@@ -163,37 +196,39 @@ pub fn nested_if_statements(
let Some((test, first_stmt)) = find_last_nested_if(body) else {
return;
};
let colon = first_colon_range(
Range::new(test.end_location.unwrap(), first_stmt.location),
checker.locator,
);
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
let nested_if = &body[0];
let fixable = !has_comments_in(
Range::new(stmt.location, nested_if.location),
checker.locator,
);
let mut diagnostic = Diagnostic::new(
NestedIfStatements,
NestedIfStatements { fixable },
colon.map_or_else(
|| Range::from_located(stmt),
|colon| Range::new(stmt.location, colon.end_location),
),
);
if checker.patch(diagnostic.kind.rule()) {
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
let nested_if = &body[0];
if !has_comments_in(
Range::new(stmt.location, nested_if.location),
checker.locator,
) {
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
Ok(fix) => {
if fix
.content
.lines()
.all(|line| line.len() <= checker.settings.line_length)
{
diagnostic.amend(fix);
}
if fixable && checker.patch(diagnostic.kind.rule()) {
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
Ok(fix) => {
if fix
.content
.lines()
.all(|line| line.len() <= checker.settings.line_length)
{
diagnostic.amend(fix);
}
Err(err) => error!("Failed to fix nested if: {err}"),
}
Err(err) => error!("Failed to fix nested if: {err}"),
}
}
checker.diagnostics.push(diagnostic);
@@ -247,16 +282,18 @@ pub fn return_bool_condition_directly(checker: &mut Checker, stmt: &Stmt) {
}
let condition = unparse_expr(test, checker.stylist);
let mut diagnostic = Diagnostic::new(
ReturnBoolConditionDirectly { cond: condition },
Range::from_located(stmt),
);
if checker.patch(diagnostic.kind.rule())
&& matches!(if_return, Bool::True)
let fixable = matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !has_comments(stmt, checker.locator)
{
&& (matches!(test.node, ExprKind::Compare { .. }) || checker.is_builtin("bool"));
let mut diagnostic = Diagnostic::new(
ReturnBoolConditionDirectly { condition, fixable },
Range::from_located(stmt),
);
if fixable && checker.patch(diagnostic.kind.rule()) {
if matches!(test.node, ExprKind::Compare { .. }) {
// If the condition is a comparison, we can replace it with the condition.
diagnostic.amend(Fix::replacement(
unparse_stmt(
&create_stmt(StmtKind::Return {
@@ -267,7 +304,9 @@ pub fn return_bool_condition_directly(checker: &mut Checker, stmt: &Stmt) {
stmt.location,
stmt.end_location.unwrap(),
));
} else if checker.is_builtin("bool") {
} else {
// Otherwise, we need to wrap the condition in a call to `bool`. (We've already
// verified, above, that `bool` is a builtin.)
diagnostic.amend(Fix::replacement(
unparse_stmt(
&create_stmt(StmtKind::Return {
@@ -394,13 +433,15 @@ pub fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&
return;
}
let fixable = !has_comments(stmt, checker.locator);
let mut diagnostic = Diagnostic::new(
UseTernaryOperator {
contents: contents.clone(),
fixable,
},
Range::from_located(stmt),
);
if checker.patch(diagnostic.kind.rule()) && !has_comments(stmt, checker.locator) {
if fixable && checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
contents,
stmt.location,
@@ -525,13 +566,15 @@ pub fn use_dict_get_with_default(
return;
}
let fixable = !has_comments(stmt, checker.locator);
let mut diagnostic = Diagnostic::new(
DictGetWithDefault {
contents: contents.clone(),
fixable,
},
Range::from_located(stmt),
);
if checker.patch(diagnostic.kind.rule()) && !has_comments(stmt, checker.locator) {
if fixable && checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
contents,
stmt.location,

View File

@@ -1,18 +1,24 @@
use log::error;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Located, Stmt, StmtKind, Withitem};
use super::fix_with;
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::helpers::{first_colon_range, has_comments_in};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::AlwaysAutofixableViolation;
use crate::violation::{AutofixKind, Availability, Violation};
use super::fix_with;
define_violation!(
pub struct MultipleWithStatements;
pub struct MultipleWithStatements {
pub fixable: bool,
}
);
impl AlwaysAutofixableViolation for MultipleWithStatements {
impl Violation for MultipleWithStatements {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
format!(
@@ -21,8 +27,13 @@ impl AlwaysAutofixableViolation for MultipleWithStatements {
)
}
fn autofix_title(&self) -> String {
"Combine `with` statements".to_string()
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let MultipleWithStatements { fixable, .. } = self;
if *fixable {
Some(|_| format!("Combine `with` statements"))
} else {
None
}
}
}
@@ -60,35 +71,33 @@ pub fn multiple_with_statements(
),
checker.locator,
);
let fixable = !has_comments_in(
Range::new(with_stmt.location, with_body[0].location),
checker.locator,
);
let mut diagnostic = Diagnostic::new(
MultipleWithStatements,
MultipleWithStatements { fixable },
colon.map_or_else(
|| Range::from_located(with_stmt),
|colon| Range::new(with_stmt.location, colon.end_location),
),
);
if checker.patch(diagnostic.kind.rule()) {
let nested_with = &with_body[0];
if !has_comments_in(
Range::new(with_stmt.location, nested_with.location),
if fixable && checker.patch(diagnostic.kind.rule()) {
match fix_with::fix_multiple_with_statements(
checker.locator,
checker.stylist,
with_stmt,
) {
match fix_with::fix_multiple_with_statements(
checker.locator,
checker.stylist,
with_stmt,
) {
Ok(fix) => {
if fix
.content
.lines()
.all(|line| line.len() <= checker.settings.line_length)
{
diagnostic.amend(fix);
}
Ok(fix) => {
if fix
.content
.lines()
.all(|line| line.len() <= checker.settings.line_length)
{
diagnostic.amend(fix);
}
Err(err) => error!("Failed to fix nested with: {err}"),
}
Err(err) => error!("Failed to fix nested with: {err}"),
}
}
checker.diagnostics.push(diagnostic);

View File

@@ -10,8 +10,7 @@ use crate::cst::matchers::{match_comparison, match_expression};
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::{Locator, Stylist};
use crate::violation::{Availability, Violation};
use crate::AutofixKind;
use crate::violation::{AutofixKind, Availability, Violation};
define_violation!(
pub struct YodaConditions {

View File

@@ -1,9 +1,10 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 2
column: 0
@@ -23,7 +24,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 7
column: 0
@@ -44,7 +46,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 15
column: 0
@@ -64,7 +67,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: false
location:
row: 20
column: 0
@@ -74,7 +78,8 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 26
column: 0
@@ -95,7 +100,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 51
column: 4
@@ -125,7 +131,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 67
column: 0
@@ -155,7 +162,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 83
column: 4
@@ -177,7 +185,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 90
column: 0
@@ -199,7 +208,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements: ~
NestedIfStatements:
fixable: true
location:
row: 117
column: 4

View File

@@ -1,10 +1,11 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
ReturnBoolConditionDirectly:
cond: a
condition: a
fixable: true
location:
row: 3
column: 4
@@ -23,7 +24,8 @@ expression: diagnostics
parent: ~
- kind:
ReturnBoolConditionDirectly:
cond: a == b
condition: a == b
fixable: true
location:
row: 11
column: 4
@@ -42,7 +44,8 @@ expression: diagnostics
parent: ~
- kind:
ReturnBoolConditionDirectly:
cond: b
condition: b
fixable: true
location:
row: 21
column: 4
@@ -61,7 +64,8 @@ expression: diagnostics
parent: ~
- kind:
ReturnBoolConditionDirectly:
cond: b
condition: b
fixable: true
location:
row: 32
column: 8
@@ -80,7 +84,8 @@ expression: diagnostics
parent: ~
- kind:
ReturnBoolConditionDirectly:
cond: a
condition: a
fixable: false
location:
row: 57
column: 4
@@ -91,7 +96,8 @@ expression: diagnostics
parent: ~
- kind:
ReturnBoolConditionDirectly:
cond: a
condition: a
fixable: false
location:
row: 83
column: 4

View File

@@ -1,10 +1,11 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
UseTernaryOperator:
contents: b = c if a else d
fixable: true
location:
row: 2
column: 0
@@ -24,6 +25,7 @@ expression: diagnostics
- kind:
UseTernaryOperator:
contents: abc = x if x > 0 else -x
fixable: false
location:
row: 58
column: 0
@@ -35,6 +37,7 @@ expression: diagnostics
- kind:
UseTernaryOperator:
contents: b = cccccccccccccccccccccccccccccccccccc if a else ddddddddddddddddddddddddddddddddddddd
fixable: true
location:
row: 82
column: 0
@@ -54,6 +57,7 @@ expression: diagnostics
- kind:
UseTernaryOperator:
contents: exitcode = 0 if True else 1
fixable: false
location:
row: 97
column: 0
@@ -65,6 +69,7 @@ expression: diagnostics
- kind:
UseTernaryOperator:
contents: x = 3 if True else 5
fixable: false
location:
row: 104
column: 0
@@ -76,6 +81,7 @@ expression: diagnostics
- kind:
UseTernaryOperator:
contents: x = 3 if True else 5
fixable: false
location:
row: 109
column: 0

View File

@@ -1,9 +1,10 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 2
column: 0
@@ -23,7 +24,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 7
column: 0
@@ -44,7 +46,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: false
location:
row: 13
column: 0
@@ -54,7 +57,8 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 19
column: 0
@@ -75,7 +79,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 53
column: 4
@@ -105,7 +110,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 68
column: 0
@@ -128,7 +134,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 76
column: 0
@@ -151,7 +158,8 @@ expression: diagnostics
column: 0
parent: ~
- kind:
MultipleWithStatements: ~
MultipleWithStatements:
fixable: true
location:
row: 84
column: 0

View File

@@ -1,10 +1,11 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
DictGetWithDefault:
contents: "var = a_dict.get(key, \"default1\")"
fixable: true
location:
row: 6
column: 0
@@ -24,6 +25,7 @@ expression: diagnostics
- kind:
DictGetWithDefault:
contents: "var = a_dict.get(key, \"default2\")"
fixable: true
location:
row: 12
column: 0
@@ -43,6 +45,7 @@ expression: diagnostics
- kind:
DictGetWithDefault:
contents: "var = a_dict.get(key, val1 + val2)"
fixable: true
location:
row: 18
column: 0
@@ -62,6 +65,7 @@ expression: diagnostics
- kind:
DictGetWithDefault:
contents: "var = a_dict.get(keys[idx], \"default\")"
fixable: true
location:
row: 24
column: 0
@@ -81,6 +85,7 @@ expression: diagnostics
- kind:
DictGetWithDefault:
contents: "var = dicts[idx].get(key, \"default\")"
fixable: true
location:
row: 30
column: 0
@@ -100,6 +105,7 @@ expression: diagnostics
- kind:
DictGetWithDefault:
contents: "vars[idx] = a_dict.get(key, \"default\")"
fixable: true
location:
row: 36
column: 0

View File

@@ -1,11 +1,19 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Stmt;
use std::path::Path;
use rustpython_parser::ast::{Stmt, StmtKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ruff_macros::{define_violation, derive_message_formats};
use ruff_python::string::is_lower_with_underscore;
use crate::ast::helpers::{create_stmt, unparse_stmt};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::violation::Violation;
use crate::source_code::Stylist;
use crate::violation::{AutofixKind, Availability, Violation};
pub type Settings = Strictness;
@@ -20,34 +28,145 @@ pub enum Strictness {
}
define_violation!(
pub struct RelativeImports(pub Strictness);
/// ## What it does
/// Checks for relative imports.
///
/// ## Why is this bad?
/// Absolute imports, or relative imports from siblings, are recommended by [PEP 8](https://peps.python.org/pep-0008/#imports):
///
/// > Absolute imports are recommended, as they are usually more readable and tend to be better behaved...
/// > ```python
/// > import mypkg.sibling
/// > from mypkg import sibling
/// > from mypkg.sibling import example
/// > ```
/// > However, explicit relative imports are an acceptable alternative to absolute imports,
/// > especially when dealing with complex package layouts where using absolute imports would be
/// > unnecessarily verbose:
/// > ```python
/// > from . import sibling
/// > from .sibling import example
/// > ```
///
/// Note that degree of strictness packages can be specified via the
/// [`strictness`](https://github.com/charliermarsh/ruff#strictness)
/// configuration option, which allows banning all relative imports (`strictness = "all"`)
/// or only those that extend into the parent module or beyond (`strictness = "parents"`,
/// the default).
///
/// ## Example
/// ```python
/// from .. import foo
/// ```
///
/// Use instead:
/// ```python
/// from mypkg import foo
/// ```
pub struct RelativeImports {
pub strictness: Strictness,
}
);
impl Violation for RelativeImports {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let RelativeImports(strictness) = self;
match strictness {
match self.strictness {
Strictness::Parents => format!("Relative imports from parent modules are banned"),
Strictness::All => format!("Relative imports are banned"),
}
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
Some(|RelativeImports { strictness }| match strictness {
Strictness::Parents => {
format!("Replace relative imports from parent modules with absolute imports")
}
Strictness::All => format!("Replace relative imports with absolute imports"),
})
}
}
fn fix_banned_relative_import(
stmt: &Stmt,
level: Option<&usize>,
module: Option<&str>,
path: &Path,
stylist: &Stylist,
) -> Option<Fix> {
let base = if let Some(module) = module {
module.to_string()
} else {
String::new()
};
let mut parent = path.parent()?;
for _ in 0..*level? {
parent = parent.parent()?;
}
let module_name = parent.file_name()?.to_string_lossy().to_string();
// Require import to be a valid PEP 8 module:
// https://python.org/dev/peps/pep-0008/#package-and-module-names
if !is_lower_with_underscore(module_name.as_str()) {
return None;
}
let new_import = if base.is_empty() {
module_name
} else {
format!("{}.{}", module_name, base)
};
let content = match &stmt.node {
StmtKind::ImportFrom { names, .. } => unparse_stmt(
&create_stmt(StmtKind::ImportFrom {
module: Some(new_import),
names: names.clone(),
level: Some(0),
}),
stylist,
),
_ => return None,
};
Some(Fix::replacement(
content,
stmt.location,
stmt.end_location.unwrap(),
))
}
/// TID252
pub fn banned_relative_import(
checker: &Checker,
stmt: &Stmt,
level: Option<&usize>,
module: Option<&str>,
strictness: &Strictness,
path: &Path,
) -> Option<Diagnostic> {
let strictness_level = match strictness {
Strictness::All => 0,
Strictness::Parents => 1,
};
if level? > &strictness_level {
Some(Diagnostic::new(
RelativeImports(strictness.clone()),
let mut diagnostic = Diagnostic::new(
RelativeImports {
strictness: strictness.clone(),
},
Range::from_located(stmt),
))
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) =
fix_banned_relative_import(stmt, level, module, path, checker.stylist)
{
diagnostic.amend(fix);
};
}
Some(diagnostic)
} else {
None
}
@@ -59,12 +178,13 @@ mod tests {
use anyhow::Result;
use super::Strictness;
use crate::assert_yaml_snapshot;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
use super::Strictness;
#[test]
fn ban_parent_imports() -> Result<()> {
let diagnostics = test_path(

View File

@@ -1,65 +1,264 @@
---
source: src/rules/flake8_tidy_imports/relative_imports.rs
source: crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs
expression: diagnostics
---
- kind:
RelativeImports: all
location:
row: 1
column: 0
end_location:
row: 1
column: 21
fix: ~
parent: ~
- kind:
RelativeImports: all
location:
row: 2
column: 0
end_location:
row: 2
column: 28
fix: ~
parent: ~
- kind:
RelativeImports: all
location:
row: 4
column: 0
end_location:
row: 4
column: 21
fix: ~
parent: ~
- kind:
RelativeImports: all
location:
row: 5
column: 0
end_location:
row: 5
column: 28
fix: ~
parent: ~
- kind:
RelativeImports: all
RelativeImports:
strictness: all
location:
row: 7
column: 0
end_location:
row: 7
column: 21
fix:
content:
- from fixtures import sibling
location:
row: 7
column: 0
end_location:
row: 7
column: 21
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 8
column: 0
end_location:
row: 8
column: 28
fix:
content:
- from fixtures.sibling import example
location:
row: 8
column: 0
end_location:
row: 8
column: 28
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix:
content:
- from test import parent
location:
row: 9
column: 0
end_location:
row: 9
column: 21
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 10
column: 0
end_location:
row: 10
column: 28
fix:
content:
- from test.parent import example
location:
row: 10
column: 0
end_location:
row: 10
column: 28
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 11
column: 0
end_location:
row: 11
column: 27
fix: ~
fix:
content:
- from resources import grandparent
location:
row: 11
column: 0
end_location:
row: 11
column: 27
parent: ~
- kind:
RelativeImports: all
RelativeImports:
strictness: all
location:
row: 8
row: 12
column: 0
end_location:
row: 8
row: 12
column: 34
fix:
content:
- from resources.grandparent import example
location:
row: 12
column: 0
end_location:
row: 12
column: 34
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 13
column: 0
end_location:
row: 13
column: 26
fix:
content:
- from fixtures.parent import hello
location:
row: 13
column: 0
end_location:
row: 13
column: 26
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 14
column: 0
end_location:
row: 16
column: 19
fix:
content:
- from fixtures.parent import hello_world
location:
row: 14
column: 0
end_location:
row: 16
column: 19
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 17
column: 0
end_location:
row: 20
column: 15
fix:
content:
- from test.parent import world_hello
location:
row: 17
column: 0
end_location:
row: 20
column: 15
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 23
column: 0
end_location:
row: 23
column: 34
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 24
column: 0
end_location:
row: 24
column: 35
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 25
column: 0
end_location:
row: 25
column: 36
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 26
column: 0
end_location:
row: 26
column: 38
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 27
column: 0
end_location:
row: 27
column: 56
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 28
column: 0
end_location:
row: 28
column: 40
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 29
column: 0
end_location:
row: 29
column: 44
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: all
location:
row: 30
column: 0
end_location:
row: 30
column: 62
fix: ~
parent: ~

View File

@@ -1,45 +1,188 @@
---
source: src/rules/flake8_tidy_imports/relative_imports.rs
source: crates/ruff/src/rules/flake8_tidy_imports/relative_imports.rs
expression: diagnostics
---
- kind:
RelativeImports: parents
RelativeImports:
strictness: parents
location:
row: 4
row: 9
column: 0
end_location:
row: 4
row: 9
column: 21
fix: ~
fix:
content:
- from test import parent
location:
row: 9
column: 0
end_location:
row: 9
column: 21
parent: ~
- kind:
RelativeImports: parents
RelativeImports:
strictness: parents
location:
row: 5
row: 10
column: 0
end_location:
row: 5
row: 10
column: 28
fix: ~
fix:
content:
- from test.parent import example
location:
row: 10
column: 0
end_location:
row: 10
column: 28
parent: ~
- kind:
RelativeImports: parents
RelativeImports:
strictness: parents
location:
row: 7
row: 11
column: 0
end_location:
row: 7
row: 11
column: 27
fix: ~
fix:
content:
- from resources import grandparent
location:
row: 11
column: 0
end_location:
row: 11
column: 27
parent: ~
- kind:
RelativeImports: parents
RelativeImports:
strictness: parents
location:
row: 8
row: 12
column: 0
end_location:
row: 8
row: 12
column: 34
fix:
content:
- from resources.grandparent import example
location:
row: 12
column: 0
end_location:
row: 12
column: 34
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 17
column: 0
end_location:
row: 20
column: 15
fix:
content:
- from test.parent import world_hello
location:
row: 17
column: 0
end_location:
row: 20
column: 15
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 23
column: 0
end_location:
row: 23
column: 34
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 24
column: 0
end_location:
row: 24
column: 35
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 25
column: 0
end_location:
row: 25
column: 36
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 26
column: 0
end_location:
row: 26
column: 38
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 27
column: 0
end_location:
row: 27
column: 56
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 28
column: 0
end_location:
row: 28
column: 40
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 29
column: 0
end_location:
row: 29
column: 44
fix: ~
parent: ~
- kind:
RelativeImports:
strictness: parents
location:
row: 30
column: 0
end_location:
row: 30
column: 62
fix: ~
parent: ~

View File

@@ -28,6 +28,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"); "TCH004_8")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"); "TCH004_9")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"); "TCH004_10")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"); "TCH004_11")]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"); "TCH005")]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"); "strict")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -164,6 +164,7 @@ pub fn typing_only_runtime_import(
package,
&settings.isort.known_first_party,
&settings.isort.known_third_party,
&settings.isort.known_local_folder,
&settings.isort.extra_standard_library,
settings.target_version,
) {

View File

@@ -0,0 +1,16 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: typing.List
location:
row: 4
column: 23
end_location:
row: 4
column: 27
fix: ~
parent: ~

View File

@@ -10,6 +10,7 @@ use crate::rules::flake8_use_pathlib::violations::{
PathlibRemove, PathlibRename, PathlibReplace, PathlibRmdir, PathlibSamefile, PathlibSplitext,
PathlibStat, PathlibUnlink,
};
use crate::settings::types::PythonVersion;
pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
if let Some(diagnostic_kind) =
@@ -32,7 +33,6 @@ pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
["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()),
@@ -42,6 +42,10 @@ pub fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
["os", "path", "splitext"] => Some(PathlibSplitext.into()),
["", "open"] => Some(PathlibOpen.into()),
["py", "path", "local"] => Some(PathlibPyPath.into()),
// Python 3.9+
["os", "readlink"] if checker.settings.target_version >= PythonVersion::Py39 => {
Some(PathlibReadlink.into())
}
_ => None,
})
{

View File

@@ -27,6 +27,7 @@ enum Reason<'a> {
NonZeroLevel,
KnownFirstParty,
KnownThirdParty,
KnownLocalFolder,
ExtraStandardLibrary,
Future,
KnownStandardLibrary,
@@ -43,6 +44,7 @@ pub fn categorize(
package: Option<&Path>,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
target_version: PythonVersion,
) -> ImportType {
@@ -53,6 +55,8 @@ pub fn categorize(
(ImportType::FirstParty, Reason::KnownFirstParty)
} else if known_third_party.contains(module_base) {
(ImportType::ThirdParty, Reason::KnownThirdParty)
} else if known_local_folder.contains(module_base) {
(ImportType::LocalFolder, Reason::KnownLocalFolder)
} else if extra_standard_library.contains(module_base) {
(ImportType::StandardLibrary, Reason::ExtraStandardLibrary)
} else if module_base == "__future__" {
@@ -98,12 +102,14 @@ fn match_sources<'a>(paths: &'a [PathBuf], base: &str) -> Option<&'a Path> {
None
}
#[allow(clippy::too_many_arguments)]
pub fn categorize_imports<'a>(
block: ImportBlock<'a>,
src: &[PathBuf],
package: Option<&Path>,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
target_version: PythonVersion,
) -> BTreeMap<ImportType, ImportBlock<'a>> {
@@ -117,6 +123,7 @@ pub fn categorize_imports<'a>(
package,
known_first_party,
known_third_party,
known_local_folder,
extra_standard_library,
target_version,
);
@@ -135,6 +142,7 @@ pub fn categorize_imports<'a>(
package,
known_first_party,
known_third_party,
known_local_folder,
extra_standard_library,
target_version,
);
@@ -153,6 +161,7 @@ pub fn categorize_imports<'a>(
package,
known_first_party,
known_third_party,
known_local_folder,
extra_standard_library,
target_version,
);
@@ -171,6 +180,7 @@ pub fn categorize_imports<'a>(
package,
known_first_party,
known_third_party,
known_local_folder,
extra_standard_library,
target_version,
);

View File

@@ -122,6 +122,7 @@ pub fn format_imports(
force_wrap_aliases: bool,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
order_by_type: bool,
relative_imports_order: RelativeImportsOrder,
single_line_exclusions: &BTreeSet<String>,
@@ -155,6 +156,7 @@ pub fn format_imports(
force_wrap_aliases,
known_first_party,
known_third_party,
known_local_folder,
order_by_type,
relative_imports_order,
single_line_exclusions,
@@ -212,6 +214,7 @@ fn format_import_block(
force_wrap_aliases: bool,
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
known_local_folder: &BTreeSet<String>,
order_by_type: bool,
relative_imports_order: RelativeImportsOrder,
single_line_exclusions: &BTreeSet<String>,
@@ -229,6 +232,7 @@ fn format_import_block(
package,
known_first_party,
known_third_party,
known_local_folder,
extra_standard_library,
target_version,
);
@@ -366,6 +370,12 @@ mod tests {
Path::new("isort").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
known_local_folder: vec!["ruff".to_string()]
.into_iter()
.collect::<BTreeSet<_>>(),
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;

View File

@@ -15,6 +15,26 @@ use crate::source_code::{Locator, Stylist};
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Adds any required imports, as specified by the user, to the top of the file.
///
/// ## Why is this bad?
/// In some projects, certain imports are required to be present in all files. For
/// example, some projects assume that `from __future__ import annotations` is enabled,
/// and thus require that import to be present in all files. Omitting a "required" import
/// (as specified by the user) can cause errors or unexpected behavior.
///
/// ## Example
/// ```python
/// import typing
/// ```
///
/// Use instead:
/// ```python
/// from __future__ import annotations
///
/// import typing
/// ```
pub struct MissingRequiredImport(pub String);
);
impl AlwaysAutofixableViolation for MissingRequiredImport {

View File

@@ -19,6 +19,24 @@ use crate::source_code::{Indexer, Locator, Stylist};
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// De-duplicates, groups, and sorts imports based on the provided `isort` settings.
///
/// ## Why is this bad?
/// Consistency is good. Use a common convention for imports to make your code
/// more readable and idiomatic.
///
/// ## Example
/// ```python
/// import pandas
/// import numpy as np
/// ```
///
/// Use instead:
/// ```python
/// import numpy as np
/// import pandas
/// ```
pub struct UnsortedImports;
);
impl AlwaysAutofixableViolation for UnsortedImports {
@@ -109,6 +127,7 @@ pub fn organize_imports(
settings.isort.force_wrap_aliases,
&settings.isort.known_first_party,
&settings.isort.known_third_party,
&settings.isort.known_local_folder,
settings.isort.order_by_type,
settings.isort.relative_imports_order,
&settings.isort.single_line_exclusions,

View File

@@ -47,7 +47,7 @@ pub struct Options {
/// exactly one member. For example, this formatting would be retained,
/// rather than condensing to a single line:
///
/// ```py
/// ```python
/// from .utils import (
/// test_directory as test_directory,
/// test_id as test_id
@@ -138,6 +138,17 @@ pub struct Options {
/// A list of modules to consider third-party, regardless of whether they
/// can be identified as such via introspection of the local filesystem.
pub known_third_party: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"
known-local-folder = ["src"]
"#
)]
/// A list of modules to consider being a local folder.
/// Generally, this is reserved for relative
/// imports (from . import module).
pub known_local_folder: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = "list[str]",
@@ -247,6 +258,7 @@ pub struct Settings {
pub force_wrap_aliases: bool,
pub known_first_party: BTreeSet<String>,
pub known_third_party: BTreeSet<String>,
pub known_local_folder: BTreeSet<String>,
pub order_by_type: bool,
pub relative_imports_order: RelativeImportsOrder,
pub single_line_exclusions: BTreeSet<String>,
@@ -270,6 +282,7 @@ impl Default for Settings {
force_wrap_aliases: false,
known_first_party: BTreeSet::new(),
known_third_party: BTreeSet::new(),
known_local_folder: BTreeSet::new(),
order_by_type: true,
relative_imports_order: RelativeImportsOrder::default(),
single_line_exclusions: BTreeSet::new(),
@@ -297,6 +310,7 @@ impl From<Options> for Settings {
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
known_local_folder: BTreeSet::from_iter(options.known_local_folder.unwrap_or_default()),
order_by_type: options.order_by_type.unwrap_or(true),
relative_imports_order: options.relative_imports_order.unwrap_or_default(),
single_line_exclusions: BTreeSet::from_iter(
@@ -324,6 +338,7 @@ impl From<Settings> for Options {
force_wrap_aliases: Some(settings.force_wrap_aliases),
known_first_party: Some(settings.known_first_party.into_iter().collect()),
known_third_party: Some(settings.known_third_party.into_iter().collect()),
known_local_folder: Some(settings.known_local_folder.into_iter().collect()),
order_by_type: Some(settings.order_by_type),
relative_imports_order: Some(settings.relative_imports_order),
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),

Some files were not shown because too many files have changed in this diff Show More