Compare commits

...

104 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
Charlie Marsh
125615af12 Bump version to 0.0.244 2023-02-08 17:28:59 -05:00
Charlie Marsh
6339f8e009 Use separate exit codes for fatal errors vs. lint errors (#2670) 2023-02-08 15:21:15 -05:00
Charlie Marsh
81abc5d7d8 Move error and warning messages into log macro (#2669) 2023-02-08 14:39:09 -05:00
Charlie Marsh
75fad989f4 Add --exit-non-zero-on-fix (#2668) 2023-02-08 14:27:54 -05:00
Charlie Marsh
cb4a221905 Treat annotated assignments in class and module scopes as runtime (#2667) 2023-02-08 13:59:25 -05:00
Charlie Marsh
286d8c18dd Remove ExprKind::Call from call path collection (#2666) 2023-02-08 13:35:18 -05:00
Florian Best
124461bddf test(UP003): let type reference be the builtin (#2664) 2023-02-08 12:44:37 -05:00
Charlie Marsh
7482a4a5b8 Avoid false-positive in chained type calls (#2663) 2023-02-08 12:18:36 -05:00
Charlie Marsh
9f9f25ff7c Accommodate multiple @pytest.mark.parametrize decorators (#2662) 2023-02-08 11:13:24 -05:00
Nuno Mendes
9cd1bf9c03 doc: add documentation for TRY002 (#2655) 2023-02-08 11:04:31 -05:00
Florian Best
3862dc2626 docs: use new command line arguments (#2658) 2023-02-08 10:36:53 -05:00
Charlie Marsh
2a0927a5ef Update Discord link 2023-02-08 04:36:59 -05:00
Charlie Marsh
824c0d2680 Implement whitespace-before-comment (E261, E262, E265, E266) (#2654) 2023-02-07 23:41:32 -05:00
Charlie Marsh
f5efdd058e Implement whitespace-around-keywords (E271, E272, E273, E274) (#2653) 2023-02-07 22:31:13 -05:00
Charlie Marsh
4c35feaa18 Add documentation for eradicate, flake8-import-conventions, and flake8-no-pep420 (#2652) 2023-02-07 22:19:21 -05:00
Charlie Marsh
8261d0656e Disable autofix for flake8-print rules (#2651) 2023-02-07 21:38:57 -05:00
Charlie Marsh
a9aa96b24f Add documentation for flake8-quotes rules (#2650) 2023-02-07 21:20:24 -05:00
Charlie Marsh
367f115d83 Add color to fixable error asterisk (#2647) 2023-02-07 19:12:18 -05:00
Charlie Marsh
56398e0002 Tweak format for rule explanations (#2645) 2023-02-07 19:02:41 -05:00
Ville Skyttä
4b49fd9494 Ignore all non-.py wrt. implicit namespace package (#2640)
It's not only `.pyi` that should be exempt for this, but also for example scripts which don't have an extension, explicitly passed in command line args.
2023-02-07 18:21:59 -05:00
Charlie Marsh
271e4fda8c Create per-rule pages and link from README (#2644) 2023-02-07 18:15:05 -05:00
Charlie Marsh
f1cdd108e6 Derive explanation method on Rule struct via rustdoc (#2642)
```console
❯ cargo run rule B017
    Finished dev [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/ruff rule B017`
no-assert-raises-exception

Code: B017 (flake8-bugbear)

### What it does
Checks for `self.assertRaises(Exception)`.

## 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`.
```
2023-02-07 17:23:29 -05:00
Charlie Marsh
8fd29b3b60 Remove dependency on "unparse" feature (#2641) 2023-02-07 17:23:09 -05:00
Charlie Marsh
e427171323 Unify imports from rustpython_parser::ast (#2639) 2023-02-07 16:54:50 -05:00
Charlie Marsh
be08384fb0 Run cargo dev generate-all 2023-02-07 16:48:06 -05:00
Charlie Marsh
2f7f4943e3 Rename some local variables 2023-02-07 16:24:53 -05:00
Charlie Marsh
67e9ff7cc8 Reorder imports (#2638) 2023-02-07 16:22:47 -05:00
Charlie Marsh
0355ba571e Skip ternary fixes for yields and awaits (#2637) 2023-02-07 15:18:52 -05:00
Charlie Marsh
38db7fd114 Avoid boolean-trap errors in __setitem__ (#2636) 2023-02-07 15:04:33 -05:00
Charlie Marsh
8ee51eb5c6 Treat @staticmethod as higher-precedence than ABC (#2635) 2023-02-07 14:57:03 -05:00
Aarni Koskela
2bc16eb4e3 flake8-annotations: add ignore-fully-untyped (#2128)
This PR adds a configuration option to inhibit ANN* violations for functions that have no other annotations either, for easier gradual typing of a large codebase.
2023-02-07 11:35:57 -05:00
Charlie Marsh
4e36225145 Avoid no-unnecessary-dict-kwargs errors with reserved keywords (#2628) 2023-02-07 11:25:09 -05:00
Charlie Marsh
850069d0aa Avoid non-recursion in nested typing function calls (#2627) 2023-02-07 11:21:49 -05:00
Charlie Marsh
9fa98ed90b Accommodate pos-only arguments when checking self name (#2626) 2023-02-07 10:50:50 -05:00
Charlie Marsh
2b4ce78830 Delete unreferenced snapshots (#2619) 2023-02-06 23:22:41 -05:00
Colin Delahunty
7647cafe12 [pylint]: bidirectional-unicode (#2589) 2023-02-06 22:49:18 -05:00
Charlie Marsh
7686179318 Remove unused src/registry.rs 2023-02-06 22:43:25 -05:00
600 changed files with 9044 additions and 3706 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:

3
.gitignore vendored
View File

@@ -1,7 +1,8 @@
# Local cache
.ruff_cache
crates/ruff/resources/test/cpython
docs/
docs/*
!docs/rules
mkdocs.yml
.overrides

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

@@ -10,7 +10,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/charliermarsh/ruff/issues) outlining your proposed
change. You can also join us on [**Discord**](https://discord.gg/Z8KbeK24) to discuss your idea with
change. You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with
the community.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
@@ -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
@@ -38,7 +41,7 @@ cargo install cargo-insta
After cloning the repository, run Ruff locally with:
```shell
cargo run /path/to/file.py --no-cache
cargo run check /path/to/file.py --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted,
@@ -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
@@ -120,7 +124,7 @@ contain a variety of violations and non-violations designed to evaluate and demo
of your lint rule.
Run `cargo dev generate-all` to generate the code for your new fixture. Then run Ruff
locally with (e.g.) `cargo run crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
locally with (e.g.) `cargo run check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test --all`.
@@ -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.

25
Cargo.lock generated
View File

@@ -747,7 +747,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -1896,7 +1896,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"anyhow",
"bisection",
@@ -1931,7 +1931,6 @@ dependencies = [
"ruff_macros",
"ruff_python",
"rustc-hash",
"rustpython-ast",
"rustpython-common",
"rustpython-parser",
"schemars",
@@ -1953,7 +1952,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1989,7 +1988,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -1998,7 +1997,6 @@ dependencies = [
"once_cell",
"ruff",
"ruff_cli",
"rustpython-ast",
"rustpython-common",
"rustpython-parser",
"schemars",
@@ -2010,9 +2008,9 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"once_cell",
"itertools",
"proc-macro2",
"quote",
"syn",
@@ -2021,7 +2019,7 @@ dependencies = [
[[package]]
name = "ruff_python"
version = "0.0.243"
version = "0.0.245"
dependencies = [
"once_cell",
"regex",
@@ -2073,17 +2071,16 @@ 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-common",
"rustpython-compiler-core",
]
[[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",
@@ -2108,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",
@@ -2125,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

242
README.md
View File

@@ -9,7 +9,7 @@
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![image](https://img.shields.io/date/1676394000?label=Jetbrains%20Ruff%20Webinar&logo=jetbrains)](https://info.jetbrains.com/PyCharm-Webinar-February14-2023.html)
[**Discord**](https://discord.gg/Z8KbeK24) | [**Docs**](https://beta.ruff.rs/docs/) | [**Playground**](https://play.ruff.rs/)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://beta.ruff.rs/docs/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter, written in Rust.
@@ -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)
@@ -213,16 +214,16 @@ apk add ruff
To run Ruff, try any of the following:
```shell
ruff . # Lint all files in the current directory (and any subdirectories)
ruff path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories)
ruff path/to/code/*.py # Lint all `.py` files in `/path/to/code`
ruff path/to/code/to/file.py # Lint `file.py`
ruff check . # Lint all files in the current directory (and any subdirectories)
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories)
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`
ruff check path/to/code/to/file.py # Lint `file.py`
```
You can run Ruff in `--watch` mode to automatically re-run on-change:
```shell
ruff path/to/code/ --watch
ruff check path/to/code/ --watch
```
Ruff also works with [pre-commit](https://pre-commit.com):
@@ -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.243'
rev: 'v0.0.245'
hooks:
- id: ruff
```
@@ -377,7 +378,7 @@ Some configuration settings can be provided via the command-line, such as those
rule enablement and disablement, file discovery, logging level, and more:
```shell
ruff path/to/code/ --select F401 --select F403 --quiet
ruff check path/to/code/ --select F401 --select F403 --quiet
```
See `ruff help` for more on Ruff's top-level commands:
@@ -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
@@ -475,6 +468,8 @@ Miscellaneous:
The name of the file when passing it through stdin
-e, --exit-zero
Exit with status code "0", even upon detecting lint violations
--exit-non-zero-on-fix
Exit with a non-zero status code if any files were modified via autofix, even if no lint violations remain
Log levels:
-v, --verbose Enable verbose logging
@@ -533,7 +528,7 @@ By default, Ruff will also skip any files that are omitted via `.ignore`, `.giti
`.git/info/exclude`, and global `gitignore` files (see: [`respect-gitignore`](#respect-gitignore)).
Files that are passed to `ruff` directly are always linted, regardless of the above criteria.
For example, `ruff /path/to/excluded/file.py` will always lint `file.py`.
For example, `ruff check /path/to/excluded/file.py` will always lint `file.py`.
### Rule resolution
@@ -557,9 +552,9 @@ select = ["E", "F"]
ignore = ["F401"]
```
Running `ruff --select F401` would result in Ruff enforcing `F401`, and no other rules.
Running `ruff check --select F401` would result in Ruff enforcing `F401`, and no other rules.
Running `ruff --extend-select B` would result in Ruff enforcing the `E`, `F`, and `B` rules, with
Running `ruff check --extend-select B` would result in Ruff enforcing the `E`, `F`, and `B` rules, with
the exception of `F401`.
### Suppressing errors
@@ -604,15 +599,15 @@ Ruff supports several workflows to aid in `noqa` management.
First, Ruff provides a special rule code, `RUF100`, to enforce that your `noqa` directives are
"valid", in that the violations they _say_ they ignore are actually being triggered on that line (and
thus suppressed). You can run `ruff /path/to/file.py --extend-select RUF100` to flag unused `noqa`
thus suppressed). You can run `ruff check /path/to/file.py --extend-select RUF100` to flag unused `noqa`
directives.
Second, Ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
You can run `ruff /path/to/file.py --extend-select RUF100 --fix` to automatically remove unused
You can run `ruff check /path/to/file.py --extend-select RUF100 --fix` to automatically remove unused
`noqa` directives.
Third, Ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to Ruff. You can run `ruff /path/to/file.py --add-noqa` to automatically
migrating a new codebase to Ruff. You can run `ruff check /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate rule codes.
#### Action comments
@@ -625,6 +620,25 @@ configuration.
See the [`isort` documentation](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
for more.
#### Exit codes
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.
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.
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
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.
<!-- End section: Configuration -->
## Supported Rules
@@ -650,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 | |
@@ -704,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}` | |
@@ -731,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)
@@ -739,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)
@@ -794,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 | |
@@ -882,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)
@@ -900,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: "{}" | |
@@ -909,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. | |
@@ -951,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 | no-assert-raises-exception | `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 | |
@@ -1076,7 +1096,7 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ICN001 | import-alias-is-not-conventional | `{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)
@@ -1099,7 +1119,7 @@ For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| INP001 | implicit-namespace-package | 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)
@@ -1121,8 +1141,16 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| T201 | print-found | `print` found | 🛠 |
| T203 | p-print-found | `pprint` found | 🛠 |
| 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)
@@ -1162,10 +1190,10 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| Q000 | bad-quotes-inline-string | Double quotes found but single quotes preferred | 🛠 |
| Q001 | bad-quotes-multiline-string | Double quote multiline found but single quotes preferred | 🛠 |
| Q002 | bad-quotes-docstring | Double quote docstring found but single quotes preferred | 🛠 |
| Q003 | avoid-quote-escape | 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)
@@ -1190,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 | 🛠 |
@@ -1221,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)
@@ -1285,7 +1313,7 @@ For more, see [eradicate](https://pypi.org/project/eradicate/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ERA001 | commented-out-code | 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)
@@ -1332,12 +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 | Contains control characters that can permit obfuscated code | |
#### Refactor (PLR)
@@ -1367,7 +1398,7 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TRY002 | raise-vanilla-class | 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 | |
@@ -1400,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. -->
@@ -1561,11 +1592,11 @@ let g:ale_fixers = {
```yaml
tools:
python-ruff: &python-ruff
lint-command: "ruff --config ~/myconfigs/linters/ruff.toml --quiet ${INPUT}"
lint-command: "ruff check --config ~/myconfigs/linters/ruff.toml --quiet ${INPUT}"
lint-stdin: true
lint-formats:
- "%f:%l:%c: %m"
format-command: "ruff --stdin-filename ${INPUT} --config ~/myconfigs/linters/ruff.toml --fix --exit-zero --quiet -"
format-command: "ruff check --stdin-filename ${INPUT} --config ~/myconfigs/linters/ruff.toml --fix --exit-zero --quiet -"
format-stdin: true
```
@@ -1627,7 +1658,7 @@ jobs:
pip install ruff
# Include `--format=github` to enable automatic inline annotations.
- name: Run Ruff
run: ruff --format=github .
run: ruff check --format=github .
```
<!-- End section: Editor Integrations -->
@@ -1682,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/)
@@ -1893,7 +1925,7 @@ matter how they're provided, which avoids accidental incompatibilities and simpl
### How can I tell what settings Ruff is using to check my code?
Run `ruff /path/to/code.py --show-settings` to view the resolved settings for a given file.
Run `ruff check /path/to/code.py --show-settings` to view the resolved settings for a given file.
### I want to use Ruff, but I don't want to use `pyproject.toml`. Is that possible?
@@ -1953,20 +1985,29 @@ 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
Contributions are welcome and highly appreciated. To get started, check out the
[**contributing guidelines**](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md). You
can also join us on [**Discord**](https://discord.gg/Z8KbeK24).
can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Support
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/charliermarsh/ruff/issues),
or feel free to [**open a new one**](https://github.com/charliermarsh/ruff/issues/new).
You can also ask for help on [**Discord**](https://discord.gg/Z8KbeK24).
You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Reference
@@ -2250,8 +2291,8 @@ fix-only = true
#### [`fixable`](#fixable)
A list of rule codes or prefixes to consider autofixable. By default, all rules are
considered autofixable.
A list of rule codes or prefixes to consider autofixable. By default,
all rules are considered autofixable.
**Default value**: `["A", "ANN", "ARG", "B", "BLE", "C", "COM", "D", "DTZ", "E", "EM", "ERA", "EXE", "F", "FBT", "G", "I", "ICN", "INP", "ISC", "N", "PD", "PGH", "PIE", "PL", "PT", "PTH", "Q", "RET", "RUF", "S", "SIM", "T", "TCH", "TID", "TRY", "UP", "W", "YTT"]`
@@ -2662,6 +2703,25 @@ allow-star-arg-any = true
---
#### [`ignore-fully-untyped`](#ignore-fully-untyped)
Whether to suppress `ANN*` rules for any declaration
that hasn't been typed at all.
This makes it easier to gradually add types to a codebase.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-annotations]
ignore-fully-untyped = true
```
---
#### [`mypy-init-return`](#mypy-init-return)
Whether to allow the omission of a return type hint for `__init__` if at
@@ -2724,8 +2784,9 @@ suppress-none-returning = true
#### [`check-typed-exception`](#check-typed-exception)
Whether to disallow `try`-`except`-`pass` (`S110`) for specific exception types. By default,
`try`-`except`-`pass` is only disallowed for `Exception` and `BaseException`.
Whether to disallow `try`-`except`-`pass` (`S110`) for specific
exception types. By default, `try`-`except`-`pass` is only
disallowed for `Exception` and `BaseException`.
**Default value**: `false`
@@ -2959,8 +3020,8 @@ The following values are supported:
* `csv` — a comma-separated list, e.g.
`@pytest.mark.parametrize('name1,name2', ...)`
* `tuple` (default) — e.g.
`@pytest.mark.parametrize(('name1', 'name2'), ...)`
* `tuple` (default) — e.g. `@pytest.mark.parametrize(('name1', 'name2'),
...)`
* `list` — e.g. `@pytest.mark.parametrize(['name1', 'name2'], ...)`
**Default value**: `tuple`
@@ -2981,10 +3042,10 @@ parametrize-names-type = "list"
Expected type for each row of values in `@pytest.mark.parametrize` in
case of multiple parameters. The following values are supported:
* `tuple` (default) — e.g.
`@pytest.mark.parametrize(('name1', 'name2'), [(1, 2), (3, 4)])`
* `list` — e.g.
`@pytest.mark.parametrize(('name1', 'name2'), [[1, 2], [3, 4]])`
* `tuple` (default) — e.g. `@pytest.mark.parametrize(('name1', 'name2'),
[(1, 2), (3, 4)])`
* `list` — e.g. `@pytest.mark.parametrize(('name1', 'name2'), [[1, 2],
[3, 4]])`
**Default value**: `tuple`
@@ -3354,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
@@ -3416,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
@@ -3747,7 +3827,8 @@ allow-magic-value-types = ["int"]
#### [`max-args`](#max-args)
Maximum number of arguments allowed for a function or method definition (see: `PLR0913`).
Maximum number of arguments allowed for a function or method definition
(see: `PLR0913`).
**Default value**: `5`
@@ -3764,7 +3845,8 @@ max-args = 5
#### [`max-branches`](#max-branches)
Maximum number of branches allowed for a function or method body (see: `PLR0912`).
Maximum number of branches allowed for a function or method body (see:
`PLR0912`).
**Default value**: `12`
@@ -3781,7 +3863,8 @@ max-branches = 12
#### [`max-returns`](#max-returns)
Maximum number of return statements allowed for a function or method body (see `PLR0911`)
Maximum number of return statements allowed for a function or method
body (see `PLR0911`)
**Default value**: `6`
@@ -3798,7 +3881,8 @@ max-returns = 6
#### [`max-statements`](#max-statements)
Maximum number of statements allowed for a function or method body (see: `PLR0915`).
Maximum number of statements allowed for a function or method body (see:
`PLR0915`).
**Default value**: `50`

View File

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

View File

@@ -5,7 +5,9 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use ruff::flake8_to_ruff::{self, ExternalConfig};
use ruff::logging::{set_up_logging, LogLevel};
#[derive(Parser)]
#[command(
@@ -27,6 +29,8 @@ struct Args {
}
fn main() -> Result<()> {
set_up_logging(&LogLevel::Default)?;
let args = Args::parse();
// Read the INI file.

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.243"
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,12 +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.243", path = "../ruff_macros" }
ruff_python = { version = "0.0.243", 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-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
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,44 @@
"""Test case expected to be run with `ignore_fully_untyped = True`."""
def ok_fully_untyped_1(a, b):
pass
def ok_fully_untyped_2():
pass
def ok_fully_typed_1(a: int, b: int) -> int:
pass
def ok_fully_typed_2() -> int:
pass
def ok_fully_typed_3(a: int, *args: str, **kwargs: str) -> int:
pass
def error_partially_typed_1(a: int, b):
pass
def error_partially_typed_2(a: int, b) -> int:
pass
def error_partially_typed_3(a: int, b: int):
pass
class X:
def ok_untyped_method_with_arg(self, a):
pass
def ok_untyped_method(self):
pass
def error_typed_self(self: X):
pass

View File

@@ -53,3 +53,8 @@ def foo():
return True
else:
return
# Error (on the argument, but not the return type)
def foo(a):
a = 2 + 2

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

@@ -57,3 +57,12 @@ dict.fromkeys(("world",), True)
{}.deploy(True, False)
getattr(someobj, attrname, False)
mylist.index(True)
class Registry:
def __init__(self) -> None:
self._switches = [False] * len(Switch)
# FBT001: Boolean positional arg in function definition
def __setitem__(self, switch: Switch, value: bool) -> None:
self._switches[switch.value] = value

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env python3
print("Hello, INP001!")

View File

@@ -17,3 +17,4 @@ foo(**{"1foo": True})
foo(**{buzz: True})
foo(**{"": True})
foo(**{f"buzz__{bar}": True})
abc(**{"for": 3})

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

@@ -75,3 +75,9 @@ def test_csv_name_list_of_lists(param1, param2):
)
def test_single_list_of_lists(param):
...
@pytest.mark.parametrize("a", [1, 2])
@pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
def test_multiple_decorators(a, b, c):
pass

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

@@ -110,3 +110,11 @@ if True: # Foo
x = 3
else:
x = 5
# OK
def f():
if True:
x = yield 3
else:
x = yield 5

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,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Tuple, List, Dict
x: Tuple
class C:
x: List
def f():
x: Dict

View File

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

View File

@@ -0,0 +1,14 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Tuple, List, Dict
x: Tuple
class C:
x: List
def f():
x: Dict

View File

@@ -1,5 +1,5 @@
from abc import abstractmethod
from typing import overload
from typing import overload, cast
from typing_extensions import override
@@ -195,3 +195,10 @@ class C:
def __exit__(self, exc_type, exc_value, traceback) -> None:
print("Hello, world!")
###
# Used arguments on chained cast.
###
def f(x: None) -> None:
_ = cast(Any, _identity)(x=x)

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

@@ -46,6 +46,10 @@ class MetaClass(ABCMeta):
def good_method(cls):
pass
@staticmethod
def static_method(not_cls) -> bool:
return False
def func(x):
return x

View File

@@ -51,3 +51,11 @@ class MetaClass(ABCMeta):
def func(x):
return x
class PosOnlyClass:
def good_method_pos_only(self, blah, /, something: str):
pass
def bad_method_pos_only(this, blah, /, self, something: str):
pass

View File

@@ -0,0 +1,66 @@
#: E261:1:5
pass # an inline comment
#: E262:1:12
x = x + 1 #Increment x
#: E262:1:12
x = x + 1 # Increment x
#: E262:1:12
x = y + 1 #: Increment x
#: E265:1:1
#Block comment
a = 1
#: E265:2:1
m = 42
#! This is important
mx = 42 - 42
#: E266:3:5 E266:6:5
def how_it_feel(r):
### This is a variable ###
a = 42
### Of course it is unused
return
#: E265:1:1 E266:2:1
##if DEBUG:
## logging.error()
#: W291:1:42
#########################################
#:
#: Okay
#!/usr/bin/env python
pass # an inline comment
x = x + 1 # Increment x
y = y + 1 #: Increment x
# Block comment
a = 1
# Block comment1
# Block comment2
aaa = 1
# example of docstring (not parsed)
def oof():
"""
#foo not parsed
"""
####################################################################
# A SEPARATOR #
####################################################################
# ################################################################ #
# ####################### another separator ###################### #
# ################################################################ #
#: E262:3:9
# -*- coding: utf8 -*-
#  (One space one NBSP) Ok for block comment
a = 42 #  (One space one NBSP)
#: E262:2:9
# (Two spaces) Ok for block comment
a = 42 # (Two spaces)

View File

@@ -0,0 +1,58 @@
#: Okay
True and False
#: E271
True and False
#: E272
True and False
#: E271
if 1:
#: E273
True and False
#: E273 E274
True and False
#: E271
a and b
#: E271
1 and b
#: E271
a and 2
#: E271 E272
1 and b
#: E271 E272
a and 2
#: E272
this and False
#: E273
a and b
#: E274
a and b
#: E273 E274
this and False
#: Okay
from u import (a, b)
from v import c, d
#: E271
from w import (e, f)
#: E275
from w import(e, f)
#: E275
from importable.module import(e, f)
#: E275
try:
from importable.module import(e, f)
except ImportError:
pass
#: E275
if(foo):
pass
else:
pass
#: Okay
matched = {"true": True, "false": False}
#: E275:2:11
if True:
assert(1)
#: Okay
def f():
print((yield))
x = (yield)

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

@@ -0,0 +1,28 @@
# E2502
print("שלום‬")
# E2502
example = "x" * 100 # "x" is assigned
# E2502
if access_level != "none": # Check if admin ' and access_level != 'user
print("You are an admin.")
# E2502
def subtract_funds(account: str, amount: int):
"""Subtract funds from bank account then """
return
bank[account] -= amount
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

@@ -4,6 +4,9 @@ type(0)
type(0.0)
type(0j)
# OK
type(arg)(" ")
# OK
y = x.dtype.type(0.0)

View File

@@ -1,9 +1,8 @@
use std::cmp::Ordering;
use rustc_hash::FxHashMap;
use rustpython_ast::ExcepthandlerKind::ExceptHandler;
use rustpython_ast::Stmt;
use rustpython_parser::ast::StmtKind;
use rustpython_parser::ast::ExcepthandlerKind::ExceptHandler;
use rustpython_parser::ast::{Stmt, StmtKind};
use crate::ast::types::RefEquality;

View File

@@ -1,4 +1,4 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
pub fn name(stmt: &Stmt) -> &str {
match &stmt.node {

View File

@@ -2,7 +2,7 @@
//! ability to compare expressions for equality (via [`Eq`] and [`Hash`]).
use num_bigint::BigInt;
use rustpython_ast::{
use rustpython_parser::ast::{
Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Keyword,
Operator, Unaryop,
};

View File

@@ -1,6 +1,6 @@
use rustpython_ast::Expr;
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;
@@ -26,11 +26,23 @@ pub fn classify(
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
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])
@@ -38,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))
@@ -46,16 +58,6 @@ pub fn classify(
})
{
FunctionType::ClassMethod
} else 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))
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method

View File

@@ -1,6 +1,6 @@
use std::hash::Hash;
use rustpython_ast::Expr;
use rustpython_parser::ast::Expr;
use crate::ast::comparable::ComparableExpr;

View File

@@ -5,7 +5,7 @@ use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{
use rustpython_parser::ast::{
Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, KeywordData,
Located, Location, Stmt, StmtKind,
};
@@ -30,23 +30,29 @@ pub fn create_stmt(node: StmtKind) -> Stmt {
Stmt::new(Location::default(), Location::default(), node)
}
/// Generate source code from an `Expr`.
/// Generate source code from an [`Expr`].
pub fn unparse_expr(expr: &Expr, stylist: &Stylist) -> String {
let mut generator: Generator = stylist.into();
generator.unparse_expr(expr, 0);
generator.generate()
}
/// Generate source code from an `Stmt`.
/// Generate source code from a [`Stmt`].
pub fn unparse_stmt(stmt: &Stmt, stylist: &Stylist) -> String {
let mut generator: Generator = stylist.into();
generator.unparse_stmt(stmt);
generator.generate()
}
/// Generate source code from an [`Constant`].
pub fn unparse_constant(constant: &Constant, stylist: &Stylist) -> String {
let mut generator: Generator = stylist.into();
generator.unparse_constant(constant);
generator.generate()
}
fn collect_call_path_inner<'a>(expr: &'a Expr, parts: &mut CallPath<'a>) -> bool {
match &expr.node {
ExprKind::Call { func, .. } => collect_call_path_inner(func, parts),
ExprKind::Attribute { value, attr, .. } => {
if collect_call_path_inner(value, parts) {
parts.push(attr);
@@ -562,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) {
@@ -661,8 +678,8 @@ pub fn to_call_path(target: &str) -> CallPath {
/// Create a module path from a (package, path) pair.
///
/// For example, if the package is `foo/bar` and the path is `foo/bar/baz.py`, the call path is
/// `["baz"]`.
/// For example, if the package is `foo/bar` and the path is `foo/bar/baz.py`,
/// the call path is `["baz"]`.
pub fn to_module_path(package: &Path, path: &Path) -> Option<Vec<String>> {
path.strip_prefix(package.parent()?)
.ok()?
@@ -1132,7 +1149,7 @@ pub fn is_logger_candidate(func: &Expr) -> bool {
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use rustpython_parser::parser;
use crate::ast::helpers::{

View File

@@ -1,7 +1,6 @@
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use rustpython_ast::{Cmpop, Located};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located, Stmt, StmtKind};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
@@ -340,7 +339,7 @@ pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
#[cfg(test)]
mod tests {
use rustpython_ast::{Cmpop, Location};
use rustpython_parser::ast::{Cmpop, Location};
use crate::ast::operations::{locate_cmpops, LocatedCmpop};

View File

@@ -2,8 +2,7 @@ use std::ops::Deref;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustc_hash::FxHashMap;
use rustpython_ast::{Arguments, Expr, Keyword, Stmt};
use rustpython_parser::ast::{Located, Location};
use rustpython_parser::ast::{Arguments, Expr, Keyword, Located, Location, Stmt};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
@@ -89,8 +88,9 @@ pub struct Scope<'a> {
pub uses_locals: bool,
/// A map from bound name to binding index, for live bindings.
pub bindings: FxHashMap<&'a str, usize>,
/// A map from bound name to binding index, for bindings that were created in the scope but
/// rebound (and thus overridden) later on in the same scope.
/// A map from bound name to binding index, for bindings that were created
/// in the scope but rebound (and thus overridden) later on in the same
/// scope.
pub rebounds: FxHashMap<&'a str, Vec<usize>>,
}

View File

@@ -1,6 +1,5 @@
use rustpython_ast::{Expr, ExprKind};
use ruff_python::typing::{PEP_585_BUILTINS_ELIGIBLE, PEP_593_SUBSCRIPTS, SUBSCRIPTS};
use rustpython_parser::ast::{Expr, ExprKind};
use crate::ast::types::CallPath;

View File

@@ -1,6 +1,6 @@
use std::str::Lines;
use rustpython_ast::{Located, Location};
use rustpython_parser::ast::{Located, Location};
use crate::ast::types::Range;
use crate::source_code::Locator;

View File

@@ -429,7 +429,7 @@ pub fn remove_argument(
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use rustpython_parser::parser;
use crate::autofix::helpers::{next_stmt_break, trailing_semicolon};

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::fix::Fix;

View File

@@ -7,11 +7,10 @@ use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Comprehension, Located, Location};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
Arg, Arguments, Comprehension, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext,
ExprKind, KeywordData, Located, Location, Operator, Stmt, StmtKind, Suite,
};
use rustpython_parser::parser;
use smallvec::smallvec;
@@ -32,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};
@@ -284,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
@@ -295,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)
}
}
@@ -696,6 +685,24 @@ where
flake8_pytest_style::rules::marks(self, decorator_list);
}
if self
.settings
.rules
.enabled(&Rule::BooleanPositionalArgInFunctionDefinition)
{
flake8_boolean_trap::rules::check_positional_boolean_in_def(self, name, args);
}
if self
.settings
.rules
.enabled(&Rule::BooleanDefaultValueInFunctionDefinition)
{
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
self, name, args,
);
}
self.check_builtin_shadowing(name, stmt, true);
// Visit the decorators and arguments, but avoid the body, which will be
@@ -1048,7 +1055,7 @@ where
if self
.settings
.rules
.enabled(&Rule::ImportAliasIsNotConventional)
.enabled(&Rule::UnconventionalImportAlias)
{
if let Some(diagnostic) =
flake8_import_conventions::rules::check_conventional_import(
@@ -1219,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(),
@@ -1284,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);
@@ -1307,7 +1317,7 @@ where
if self
.settings
.rules
.enabled(&Rule::ImportAliasIsNotConventional)
.enabled(&Rule::UnconventionalImportAlias)
{
let full_name = helpers::format_import_from_member(
level.as_ref(),
@@ -1521,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,
@@ -1529,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));
}
@@ -1547,7 +1557,7 @@ where
}
}
StmtKind::With { items, body, .. } => {
if self.settings.rules.enabled(&Rule::NoAssertRaisesException) {
if self.settings.rules.enabled(&Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(self, stmt, items);
}
if self
@@ -1688,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);
}
}
@@ -1706,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);
}
@@ -1738,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);
}
}
}
@@ -1929,7 +1945,21 @@ where
value,
..
} => {
self.visit_annotation(annotation);
// If we're in a class or module scope, then the annotation needs to be available
// at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
if !self.annotations_future_enabled
&& matches!(
self.current_scope().kind,
ScopeKind::Class(..) | ScopeKind::Module
)
{
self.in_type_definition = true;
self.visit_expr(annotation);
self.in_type_definition = false;
} else {
self.visit_annotation(annotation);
}
if let Some(expr) = value {
if self.match_typing_expr(annotation, "TypeAlias") {
self.in_type_definition = true;
@@ -2285,10 +2315,10 @@ where
pyupgrade::rules::open_alias(self, expr, func);
}
if self.settings.rules.enabled(&Rule::ReplaceUniversalNewlines) {
pyupgrade::rules::replace_universal_newlines(self, expr, keywords);
pyupgrade::rules::replace_universal_newlines(self, func, keywords);
}
if self.settings.rules.enabled(&Rule::ReplaceStdoutStderr) {
pyupgrade::rules::replace_stdout_stderr(self, expr, keywords);
pyupgrade::rules::replace_stdout_stderr(self, expr, func, keywords);
}
if self.settings.rules.enabled(&Rule::OSErrorAlias) {
pyupgrade::rules::os_error_alias(self, &expr);
@@ -2319,7 +2349,7 @@ where
.rules
.enabled(&Rule::UselessContextlibSuppress)
{
flake8_bugbear::rules::useless_contextlib_suppress(self, expr, args);
flake8_bugbear::rules::useless_contextlib_suppress(self, expr, func, args);
}
if self
.settings
@@ -2342,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);
}
@@ -2375,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
@@ -2760,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) {
@@ -2782,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,
@@ -2943,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 {
@@ -2964,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);
@@ -3488,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,
@@ -3516,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,
@@ -3652,24 +3714,6 @@ where
flake8_bugbear::rules::function_call_argument_default(self, arguments);
}
// flake8-boolean-trap
if self
.settings
.rules
.enabled(&Rule::BooleanPositionalArgInFunctionDefinition)
{
flake8_boolean_trap::rules::check_positional_boolean_in_def(self, arguments);
}
if self
.settings
.rules
.enabled(&Rule::BooleanDefaultValueInFunctionDefinition)
{
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
self, arguments,
);
}
// Bind, but intentionally avoid walking default expressions, as we handle them
// upstream.
for arg in &arguments.posonlyargs {
@@ -3840,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()
@@ -3952,8 +4004,8 @@ impl<'a> Checker<'a> {
// Avoid overriding builtins.
binding
} else if matches!(self.bindings[*index].kind, BindingKind::Global) {
// If the original binding was a global, and the new binding conflicts within the
// current scope, then the new binding is also a global.
// If the original binding was a global, and the new binding conflicts within
// the current scope, then the new binding is also a global.
Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
@@ -3962,8 +4014,8 @@ impl<'a> Checker<'a> {
..binding
}
} else if matches!(self.bindings[*index].kind, BindingKind::Nonlocal) {
// If the original binding was a nonlocal, and the new binding conflicts within the
// current scope, then the new binding is also a nonlocal.
// If the original binding was a nonlocal, and the new binding conflicts within
// the current scope, then the new binding is also a nonlocal.
Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
@@ -4289,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(
@@ -4563,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)
@@ -4582,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
@@ -4633,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.
@@ -4677,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;
}
@@ -4713,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,
));
}
}
}
@@ -4805,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 {
@@ -4815,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;
}
@@ -4838,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
@@ -4972,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)
@@ -5015,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)
@@ -5076,7 +5137,12 @@ impl<'a> Checker<'a> {
&overloaded_name,
)
}) {
flake8_annotations::rules::definition(self, &definition, &visibility);
self.diagnostics
.extend(flake8_annotations::rules::definition(
self,
&definition,
&visibility,
));
}
overloaded_name = flake8_annotations::helpers::overloaded_name(self, &definition);
}
@@ -5158,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

@@ -1,12 +1,15 @@
use bisection::bisect_left;
use itertools::Itertools;
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer::LexResult;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::logical_lines::iter_logical_lines;
use crate::rules::pycodestyle::rules::{extraneous_whitespace, indentation, space_around_operator};
use crate::rules::pycodestyle::logical_lines::{iter_logical_lines, TokenFlags};
use crate::rules::pycodestyle::rules::{
extraneous_whitespace, indentation, space_around_operator, whitespace_around_keywords,
whitespace_before_comment,
};
use crate::settings::Settings;
use crate::source_code::{Locator, Stylist};
@@ -57,7 +60,7 @@ pub fn check_logical_lines(
// Generate mapping from logical to physical offsets.
let mapping_offsets = line.mapping.iter().map(|(offset, _)| *offset).collect_vec();
if line.operator {
if line.flags.contains(TokenFlags::OPERATOR) {
for (index, kind) in space_around_operator(&line.text) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
@@ -72,7 +75,10 @@ pub fn check_logical_lines(
}
}
}
if line.bracket || line.punctuation {
if line
.flags
.contains(TokenFlags::OPERATOR | TokenFlags::PUNCTUATION)
{
for (index, kind) in extraneous_whitespace(&line.text) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
@@ -87,6 +93,34 @@ pub fn check_logical_lines(
}
}
}
if line.flags.contains(TokenFlags::KEYWORD) {
for (index, kind) in whitespace_around_keywords(&line.text) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: None,
parent: None,
});
}
}
}
if line.flags.contains(TokenFlags::COMMENT) {
for (range, kind) in whitespace_before_comment(&line.tokens, locator) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location: range.location,
end_location: range.end_location,
fix: None,
parent: None,
});
}
}
}
for (index, kind) in indentation(
&line,

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

@@ -2,7 +2,7 @@
use bitflags::bitflags;
use nohash_hasher::{IntMap, IntSet};
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::registry::LintSource;

View File

@@ -1,7 +1,7 @@
//! Doc line extraction. In this context, a doc line is a line consisting of a
//! standalone comment or a constant string statement.
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::visitor;

View File

@@ -1,4 +1,4 @@
use rustpython_ast::{Expr, Stmt};
use rustpython_parser::ast::{Expr, Stmt};
#[derive(Debug, Clone)]
pub enum DefinitionKind<'a> {

View File

@@ -1,6 +1,6 @@
//! Extract docstrings from an AST.
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
use crate::visibility::{Modifier, VisibleScope};

View File

@@ -1,4 +1,4 @@
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Hash)]

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

@@ -1,6 +1,6 @@
use std::path::Path;
use rustpython_ast::Location;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer::LexResult;
use serde::Serialize;
use wasm_bindgen::prelude::*;

View File

@@ -3,6 +3,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use log::error;
use rustpython_parser::error::ParseError;
use rustpython_parser::lexer::LexResult;
@@ -17,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};
@@ -27,8 +28,9 @@ use crate::{directives, fs, rustpython_helpers};
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
/// A [`Result`]-like type that returns both data and an error. Used to return diagnostics even in
/// the face of parse errors, since many diagnostics can be generated without a full AST.
/// A [`Result`]-like type that returns both data and an error. Used to return
/// diagnostics even in the face of parse errors, since many diagnostics can be
/// generated without a full AST.
pub struct LinterResult<T> {
pub data: T,
pub error: Option<ParseError>,
@@ -147,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);
}
}
}
}
@@ -246,17 +258,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Log any parse errors.
if let Some(err) = error {
#[allow(clippy::print_stderr)]
{
eprintln!(
"{}{} {}{}{} {err:?}",
"error".red().bold(),
":".bold(),
"Failed to parse ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
error!(
"{}{}{} {err:?}",
"Failed to parse ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
}
// Add any missing `# noqa` pragmas.
@@ -270,7 +277,8 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
)
}
/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source code.
/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source
/// code.
pub fn lint_only(
contents: &str,
path: &Path,

View File

@@ -1,19 +1,18 @@
use anyhow::Result;
use colored::Colorize;
use fern;
use log::Level;
#[macro_export]
macro_rules! warn_user_once {
($($arg:tt)*) => {
use colored::Colorize;
use log::warn;
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::SeqCst) {
let message = format!("{}", format_args!($($arg)*));
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
message.bold(),
);
warn!("{}", message.bold());
}
};
}
@@ -22,13 +21,10 @@ macro_rules! warn_user_once {
macro_rules! warn_user {
($($arg:tt)*) => {
use colored::Colorize;
use log::warn;
let message = format!("{}", format_args!($($arg)*));
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
message.bold(),
);
warn!("{}", message.bold());
};
}
@@ -50,7 +46,8 @@ macro_rules! notify_user {
pub enum LogLevel {
/// No output ([`log::LevelFilter::Off`]).
Silent,
/// Only show lint violations, with no decorative output ([`log::LevelFilter::Off`]).
/// Only show lint violations, with no decorative output
/// ([`log::LevelFilter::Off`]).
Quiet,
/// All user-facing output ([`log::LevelFilter::Info`]).
#[default]
@@ -73,14 +70,32 @@ impl LogLevel {
pub fn set_up_logging(level: &LogLevel) -> Result<()> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
));
.format(|out, message, record| match record.level() {
Level::Error => {
out.finish(format_args!(
"{}{} {}",
"error".red().bold(),
":".bold(),
message
));
}
Level::Warn => {
out.finish(format_args!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
message
));
}
Level::Info | Level::Debug | Level::Trace => {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
));
}
})
.level(level.level_filter())
.level_for("globset", log::LevelFilter::Warn)

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

@@ -119,7 +119,8 @@ pub fn detect_package_roots<'a>(
mod tests {
use std::path::PathBuf;
use crate::{packaging::detect_package_root, test::test_resource_path};
use crate::packaging::detect_package_root;
use crate::test::test_resource_path;
#[test]
fn package_detection() {

View File

@@ -41,16 +41,36 @@ ruff_macros::define_rule_mapping!(
E223 => rules::pycodestyle::rules::TabBeforeOperator,
#[cfg(feature = "logical_lines")]
E224 => rules::pycodestyle::rules::TabAfterOperator,
#[cfg(feature = "logical_lines")]
E261 => rules::pycodestyle::rules::TooFewSpacesBeforeInlineComment,
#[cfg(feature = "logical_lines")]
E262 => rules::pycodestyle::rules::NoSpaceAfterInlineComment,
#[cfg(feature = "logical_lines")]
E265 => rules::pycodestyle::rules::NoSpaceAfterBlockComment,
#[cfg(feature = "logical_lines")]
E266 => rules::pycodestyle::rules::MultipleLeadingHashesForBlockComment,
#[cfg(feature = "logical_lines")]
E271 => rules::pycodestyle::rules::MultipleSpacesAfterKeyword,
#[cfg(feature = "logical_lines")]
E272 => rules::pycodestyle::rules::MultipleSpacesBeforeKeyword,
#[cfg(feature = "logical_lines")]
E273 => rules::pycodestyle::rules::TabAfterKeyword,
#[cfg(feature = "logical_lines")]
E274 => rules::pycodestyle::rules::TabBeforeKeyword,
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,
@@ -63,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,
@@ -105,8 +125,11 @@ 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,
PLC3002 => rules::pylint::rules::UnnecessaryDirectLambdaCall,
@@ -139,13 +162,13 @@ 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,
B015 => rules::flake8_bugbear::rules::UselessComparison,
B016 => rules::flake8_bugbear::rules::CannotRaiseLiteral,
B017 => rules::flake8_bugbear::rules::NoAssertRaisesException,
B017 => rules::flake8_bugbear::rules::AssertRaisesException,
B018 => rules::flake8_bugbear::rules::UselessExpression,
B019 => rules::flake8_bugbear::rules::CachedInstanceMethod,
B020 => rules::flake8_bugbear::rules::LoopVariableOverridesIterator,
@@ -204,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,
@@ -216,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,
@@ -315,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,
@@ -359,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,
@@ -387,7 +412,7 @@ ruff_macros::define_rule_mapping!(
ARG004 => rules::flake8_unused_arguments::rules::UnusedStaticMethodArgument,
ARG005 => rules::flake8_unused_arguments::rules::UnusedLambdaArgument,
// flake8-import-conventions
ICN001 => rules::flake8_import_conventions::rules::ImportAliasIsNotConventional,
ICN001 => rules::flake8_import_conventions::rules::UnconventionalImportAlias,
// flake8-datetimez
DTZ001 => rules::flake8_datetimez::rules::CallDatetimeWithoutTzinfo,
DTZ002 => rules::flake8_datetimez::rules::CallDatetimeToday,
@@ -420,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,
@@ -610,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,
@@ -725,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
@@ -741,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,
@@ -748,13 +783,21 @@ impl Rule {
#[cfg(feature = "logical_lines")]
Rule::IndentationWithInvalidMultiple
| Rule::IndentationWithInvalidMultipleComment
| Rule::MultipleLeadingHashesForBlockComment
| Rule::MultipleSpacesAfterKeyword
| Rule::MultipleSpacesAfterOperator
| Rule::MultipleSpacesBeforeKeyword
| Rule::MultipleSpacesBeforeOperator
| Rule::NoIndentedBlock
| Rule::NoIndentedBlockComment
| Rule::NoSpaceAfterBlockComment
| Rule::NoSpaceAfterInlineComment
| Rule::OverIndented
| Rule::TabAfterKeyword
| Rule::TabAfterOperator
| Rule::TabBeforeKeyword
| Rule::TabBeforeOperator
| Rule::TooFewSpacesBeforeInlineComment
| Rule::UnexpectedIndentation
| Rule::UnexpectedIndentationComment
| Rule::WhitespaceAfterOpenBracket
@@ -818,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

@@ -587,15 +587,16 @@ mod tests {
&Relativity::Parent,
&NoOpProcessor,
)?);
// src/app.py should not be excluded even if it lives in a hierarchy that should be
// excluded by virtue of the pyproject.toml having `resources/*` in it.
// src/app.py should not be excluded even if it lives in a hierarchy that should
// be excluded by virtue of the pyproject.toml having `resources/*` in
// it.
assert!(!is_file_excluded(
&package_root.join("src/app.py"),
&resolver,
&ppd,
));
// However, resources/ignored.py should be ignored, since that `resources` is beneath
// the package root.
// However, resources/ignored.py should be ignored, since that `resources` is
// beneath the package root.
assert!(is_file_excluded(
&package_root.join("resources/ignored.py"),
&resolver,

View File

@@ -1,9 +1,8 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::Location;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Location;
use super::detection::comment_contains_code;
use crate::ast::types::Range;
use crate::define_violation;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::settings::{flags, Settings};
@@ -11,6 +10,17 @@ use crate::source_code::Locator;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for commented-out Python code.
///
/// ## Why is this bad?
/// Commented-out code is dead code, and is often included inadvertently.
/// It should be removed.
///
/// ## Example
/// ```python
/// # print('foo')
/// ```
pub struct CommentedOutCode;
);
impl AlwaysAutofixableViolation for CommentedOutCode {

View File

@@ -1,14 +1,12 @@
use num_bigint::BigInt;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Located};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, Rule};
use crate::violation::Violation;
use crate::define_violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct SysVersionSlice3Referenced;
);

View File

@@ -1,5 +1,5 @@
use anyhow::{bail, Result};
use rustpython_ast::Stmt;
use rustpython_parser::ast::Stmt;
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;

View File

@@ -1,4 +1,4 @@
use rustpython_ast::{Arguments, Expr, Stmt, StmtKind};
use rustpython_parser::ast::{Arguments, Expr, Stmt, StmtKind};
use crate::ast::cast;
use crate::checkers::ast::Checker;

View File

@@ -31,7 +31,35 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
assert_yaml_snapshot!(diagnostics);
Ok(())
}
#[test]
fn ignore_fully_untyped() -> Result<()> {
let diagnostics = test_path(
Path::new("flake8_annotations/ignore_fully_untyped.py"),
&Settings {
flake8_annotations: super::settings::Settings {
ignore_fully_untyped: true,
..Default::default()
},
..Settings::for_rules(vec![
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::AnyType,
])
},
)?;
@@ -45,10 +73,8 @@ mod tests {
Path::new("flake8_annotations/suppress_dummy_args.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: true,
suppress_none_returning: false,
allow_star_arg_any: false,
..Default::default()
},
..Settings::for_rules(vec![
Rule::MissingTypeFunctionArgument,
@@ -70,9 +96,7 @@ mod tests {
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: true,
suppress_dummy_args: false,
suppress_none_returning: false,
allow_star_arg_any: false,
..Default::default()
},
..Settings::for_rules(vec![
Rule::MissingReturnTypePublicFunction,
@@ -83,7 +107,7 @@ mod tests {
])
},
)?;
assert_yaml_snapshot!(diagnostics);
insta::assert_yaml_snapshot!(diagnostics);
Ok(())
}
@@ -93,17 +117,21 @@ mod tests {
Path::new("flake8_annotations/suppress_none_returning.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: false,
suppress_none_returning: true,
allow_star_arg_any: false,
..Default::default()
},
..Settings::for_rules(vec![
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::AnyType,
])
},
)?;
@@ -117,12 +145,10 @@ mod tests {
Path::new("flake8_annotations/allow_star_arg_any.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
suppress_dummy_args: false,
suppress_none_returning: false,
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

@@ -1,24 +1,40 @@
use log::error;
use rustpython_ast::{Constant, Expr, ExprKind, Stmt};
use ruff_macros::derive_message_formats;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt};
use super::fixes;
use super::helpers::match_function_def;
use crate::ast::helpers::ReturnStatementVisitor;
use crate::ast::types::Range;
use crate::ast::visitor::Visitor;
use crate::ast::{cast, helpers};
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::registry::{Diagnostic, Rule};
use crate::violation::{AlwaysAutofixableViolation, Violation};
use crate::visibility;
use crate::visibility::Visibility;
use super::fixes;
use super::helpers::match_function_def;
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,
}
@@ -32,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,
}
@@ -45,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,
}
@@ -58,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,
}
@@ -71,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,
}
@@ -84,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,
}
@@ -97,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,
}
@@ -110,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,
}
@@ -127,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,
}
@@ -140,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,
}
@@ -153,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}`")
}
}
@@ -185,286 +433,302 @@ fn is_none_returning(body: &[Stmt]) -> bool {
}
/// ANN401
fn check_dynamically_typed<F>(checker: &mut Checker, annotation: &Expr, func: F)
where
fn check_dynamically_typed<F>(
checker: &Checker,
annotation: &Expr,
func: F,
diagnostics: &mut Vec<Diagnostic>,
) where
F: FnOnce() -> String,
{
if checker.match_typing_expr(annotation, "Any") {
checker.diagnostics.push(Diagnostic::new(
DynamicallyTypedExpression { name: func() },
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
Range::from_located(annotation),
));
};
}
/// Generate flake8-annotation checks for a given `Definition`.
pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &Visibility) {
pub fn definition(
checker: &Checker,
definition: &Definition,
visibility: &Visibility,
) -> Vec<Diagnostic> {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
match &definition.kind {
DefinitionKind::Module => {}
DefinitionKind::Package => {}
DefinitionKind::Class(_) => {}
DefinitionKind::NestedClass(_) => {}
DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) => {
let is_method = matches!(definition.kind, DefinitionKind::Method(_));
let (name, args, returns, body) = match_function_def(stmt);
let mut has_any_typed_arg = false;
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = &definition.kind
{
let is_method = matches!(definition.kind, DefinitionKind::Method(_));
let (name, args, returns, body) = match_function_def(stmt);
// Keep track of whether we've seen any typed arguments or return values.
let mut has_any_typed_arg = false; // Any argument has been typed?
let mut has_typed_return = false; // Return value has been typed?
let mut has_typed_self_or_cls = false; // Has a typed `self` or `cls` argument?
// ANN001, ANN401
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method
&& !visibility::is_staticmethod(checker, cast::decorator_list(stmt)),
),
)
{
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
// Temporary storage for diagnostics; we emit them at the end
// unless configured to suppress ANN* for declarations that are fully untyped.
let mut diagnostics = Vec::new();
// ANN001, ANN401
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method && !visibility::is_staticmethod(checker, cast::decorator_list(stmt)),
),
)
{
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(
checker,
annotation,
|| arg.node.arg.to_string(),
&mut diagnostics,
);
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
.enabled(&Rule::MissingTypeFunctionArgument)
{
check_dynamically_typed(checker, annotation, || arg.node.arg.to_string());
diagnostics.push(Diagnostic::new(
MissingTypeFunctionArgument {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker
.settings
.rules
.enabled(&Rule::MissingTypeFunctionArgument)
{
checker.diagnostics.push(Diagnostic::new(
MissingTypeFunctionArgument {
}
}
}
// ANN002, ANN401
if let Some(arg) = &args.vararg {
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::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
expr,
|| format!("*{name}"),
&mut diagnostics,
);
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeArgs) {
diagnostics.push(Diagnostic::new(
MissingTypeArgs {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
}
}
// ANN003, ANN401
if let Some(arg) = &args.kwarg {
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::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
expr,
|| format!("**{name}"),
&mut diagnostics,
);
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeKwargs) {
diagnostics.push(Diagnostic::new(
MissingTypeKwargs {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
}
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
} else {
if checker.settings.rules.enabled(&Rule::MissingTypeSelf) {
diagnostics.push(Diagnostic::new(
MissingTypeSelf {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
}
}
// ANN002, ANN401
if let Some(arg) = &args.vararg {
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)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeArgs) {
checker.diagnostics.push(Diagnostic::new(
MissingTypeArgs {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
has_typed_self_or_cls = true;
}
}
}
// ANN003, ANN401
if let Some(arg) = &args.kwarg {
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)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeKwargs) {
checker.diagnostics.push(Diagnostic::new(
MissingTypeKwargs {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
}
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&Rule::MissingTypeCls) {
checker.diagnostics.push(Diagnostic::new(
MissingTypeCls {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
} else {
if checker.settings.rules.enabled(&Rule::MissingTypeSelf) {
checker.diagnostics.push(Diagnostic::new(
MissingTypeSelf {
name: arg.node.arg.to_string(),
},
Range::from_located(arg),
));
}
}
}
}
}
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
} else if !(
// Allow omission of return annotation if the function only returns `None`
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
.enabled(&Rule::MissingReturnTypeClassMethod)
{
check_dynamically_typed(checker, expr, || name.to_string());
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
} else {
// Allow omission of return annotation if the function only returns `None`
// (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning
&& is_none_returning(body)
} else if is_method && visibility::is_staticmethod(checker, cast::decorator_list(stmt))
{
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeStaticMethod)
{
return;
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
if is_method && visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeClassMethod)
{
checker.diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
} else if is_method
&& visibility::is_staticmethod(checker, cast::decorator_list(stmt))
} else if is_method && visibility::is_init(cast::name(stmt)) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeStaticMethod)
if !(checker.settings.flake8_annotations.mypy_init_return && has_any_typed_arg)
{
checker.diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
} else if is_method && visibility::is_init(cast::name(stmt)) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
if !(checker.settings.flake8_annotations.mypy_init_return
&& has_any_typed_arg)
{
let mut diagnostic = Diagnostic::new(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
);
if checker.patch(diagnostic.kind.rule()) {
match fixes::add_return_none_annotation(checker.locator, stmt) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
checker.diagnostics.push(diagnostic);
}
}
} else if is_method && visibility::is_magic(cast::name(stmt)) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
} else {
match visibility {
Visibility::Public => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePublicFunction)
{
checker.diagnostics.push(Diagnostic::new(
MissingReturnTypePublicFunction {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
);
if checker.patch(diagnostic.kind.rule()) {
match fixes::add_return_none_annotation(checker.locator, stmt) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
Visibility::Private => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePrivateFunction)
{
checker.diagnostics.push(Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
diagnostics.push(diagnostic);
}
}
} else if is_method && visibility::is_magic(cast::name(stmt)) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
diagnostics.push(Diagnostic::new(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
} else {
match visibility {
Visibility::Public => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePublicFunction)
{
diagnostics.push(Diagnostic::new(
MissingReturnTypePublicFunction {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
}
Visibility::Private => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePrivateFunction)
{
diagnostics.push(Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
helpers::identifier_range(stmt, checker.locator),
));
}
}
}
}
}
// If settings say so, don't report any of the
// diagnostics gathered here if there were no type annotations at all.
if checker.settings.flake8_annotations.ignore_fully_untyped
&& !(has_any_typed_arg || has_typed_self_or_cls || has_typed_return)
{
vec![]
} else {
diagnostics
}
} else {
vec![]
}
}

View File

@@ -49,6 +49,15 @@ pub struct Options {
/// Whether to suppress `ANN401` for dynamically typed `*args` and
/// `**kwargs` arguments.
pub allow_star_arg_any: Option<bool>,
#[option(
default = "false",
value_type = "bool",
example = "ignore-fully-untyped = true"
)]
/// Whether to suppress `ANN*` rules for any declaration
/// that hasn't been typed at all.
/// This makes it easier to gradually add types to a codebase.
pub ignore_fully_untyped: Option<bool>,
}
#[derive(Debug, Default, Hash)]
@@ -58,6 +67,7 @@ pub struct Settings {
pub suppress_dummy_args: bool,
pub suppress_none_returning: bool,
pub allow_star_arg_any: bool,
pub ignore_fully_untyped: bool,
}
impl From<Options> for Settings {
@@ -67,6 +77,7 @@ impl From<Options> for Settings {
suppress_dummy_args: options.suppress_dummy_args.unwrap_or(false),
suppress_none_returning: options.suppress_none_returning.unwrap_or(false),
allow_star_arg_any: options.allow_star_arg_any.unwrap_or(false),
ignore_fully_untyped: options.ignore_fully_untyped.unwrap_or(false),
}
}
}
@@ -78,6 +89,7 @@ impl From<Settings> for Options {
suppress_dummy_args: Some(settings.suppress_dummy_args),
suppress_none_returning: Some(settings.suppress_none_returning),
allow_star_arg_any: Some(settings.allow_star_arg_any),
ignore_fully_untyped: Some(settings.ignore_fully_untyped),
}
}
}

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

@@ -0,0 +1,60 @@
---
source: crates/ruff/src/rules/flake8_annotations/mod.rs
expression: diagnostics
---
- kind:
MissingReturnTypePublicFunction:
name: error_partially_typed_1
location:
row: 24
column: 4
end_location:
row: 24
column: 27
fix: ~
parent: ~
- kind:
MissingTypeFunctionArgument:
name: b
location:
row: 24
column: 36
end_location:
row: 24
column: 37
fix: ~
parent: ~
- kind:
MissingTypeFunctionArgument:
name: b
location:
row: 28
column: 36
end_location:
row: 28
column: 37
fix: ~
parent: ~
- kind:
MissingReturnTypePublicFunction:
name: error_partially_typed_3
location:
row: 32
column: 4
end_location:
row: 32
column: 27
fix: ~
parent: ~
- kind:
MissingReturnTypePublicFunction:
name: error_typed_self
location:
row: 43
column: 8
end_location:
row: 43
column: 24
fix: ~
parent: ~

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:
@@ -12,8 +12,7 @@ expression: diagnostics
row: 5
column: 16
fix:
content:
- " -> None"
content: " -> None"
location:
row: 5
column: 22
@@ -31,8 +30,7 @@ expression: diagnostics
row: 11
column: 16
fix:
content:
- " -> None"
content: " -> None"
location:
row: 11
column: 27
@@ -61,8 +59,7 @@ expression: diagnostics
row: 47
column: 16
fix:
content:
- " -> None"
content: " -> None"
location:
row: 47
column: 28

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:
@@ -24,4 +24,15 @@ expression: diagnostics
column: 7
fix: ~
parent: ~
- kind:
MissingTypeFunctionArgument:
name: a
location:
row: 59
column: 8
end_location:
row: 59
column: 9
fix: ~
parent: ~

View File

@@ -1,4 +1,6 @@
use rustpython_ast::{Constant, Expr, ExprKind};
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

@@ -1,15 +1,14 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Located, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Located, StmtKind};
use crate::ast::types::Range;
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")
@@ -19,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

@@ -1,15 +1,14 @@
use crate::define_violation;
use crate::violation::Violation;
use num_traits::ToPrimitive;
use once_cell::sync::Lazy;
use ruff_macros::derive_message_formats;
use ruff_macros::{define_violation, derive_message_formats};
use rustc_hash::FxHashMap;
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Operator};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Operator};
use crate::ast::helpers::{compose_call_path, SimpleCallArgs};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct BadFilePermissions {

View File

@@ -1,15 +1,14 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind};
use crate::ast::types::Range;
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")
@@ -24,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

@@ -1,10 +1,9 @@
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violation::Violation;
use crate::define_violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct HardcodedBindAllInterfaces;
);

View File

@@ -1,11 +1,10 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{ArgData, Arguments, Expr, Located};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{ArgData, Arguments, Expr, Located};
use super::super::helpers::{matches_password_name, string_literal};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct HardcodedPasswordDefault {

View File

@@ -1,11 +1,10 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::Keyword;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Keyword;
use super::super::helpers::{matches_password_name, string_literal};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct HardcodedPasswordFuncArg {

View File

@@ -1,11 +1,10 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Constant, Expr, ExprKind};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Constant, Expr, ExprKind};
use super::super::helpers::{matches_password_name, string_literal};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct HardcodedPasswordString {

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,10 +1,9 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::Expr;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Expr;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct HardcodedTempFile {

View File

@@ -1,13 +1,12 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Constant, Expr, ExprKind, Keyword};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use super::super::helpers::string_literal;
use crate::ast::helpers::SimpleCallArgs;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct HashlibInsecureHashFunction {

View File

@@ -1,13 +1,11 @@
use crate::define_violation;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind, Keyword};
use rustpython_parser::ast::Constant;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword};
use crate::ast::helpers::SimpleCallArgs;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct Jinja2AutoescapeFalse {

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