Compare commits

...

82 Commits

Author SHA1 Message Date
Charlie Marsh
4819e19ba2 Bump version to 0.0.50 2022-10-02 20:43:30 -04:00
Charlie Marsh
622b8adb79 Avoid falling back to A003 when A001 is disabled (#302) 2022-10-02 20:43:12 -04:00
Charlie Marsh
558d9fcbe3 Enable LibCST-based autofixing for SPR001 (#297) 2022-10-02 19:58:13 -04:00
Charlie Marsh
83f18193c2 Add an end location to Check (#299) 2022-10-02 12:50:42 -04:00
Charlie Marsh
46e6a1b3be Add end locations to all nodes (#296) 2022-10-02 12:49:48 -04:00
Suguru Yamamoto
4d0d433af9 fix: Make assigns to dunder exception for E402. (#294) 2022-10-01 09:43:47 -04:00
Christian Clauss
11f7532e72 pre-commit: Validate pyproject.toml (#266) 2022-09-30 19:21:12 -04:00
Charlie Marsh
417764d309 Expose a public 'check' method (#289) 2022-09-30 11:30:37 -04:00
Charlie Marsh
1e36c109c6 Bump version to 0.0.49 2022-09-30 09:15:32 -04:00
Nikita Sobolev
3960016d55 Create .editorconfig (#290) 2022-09-30 09:15:08 -04:00
Charlie Marsh
75d669fa86 Update check ordering 2022-09-30 09:14:41 -04:00
Nikita Sobolev
20989e12ba Implement flake8-super check (#291) 2022-09-30 09:12:09 -04:00
Charlie Marsh
5a1b6c32eb Add CONTRIBUTING.md (#288) 2022-09-30 07:51:30 -04:00
Charlie Marsh
46bdcb9080 Add instructions on GitHub Actions integration 2022-09-29 19:05:02 -04:00
Charlie Marsh
d16a7252af Add instructions on PyCharm integration 2022-09-29 18:52:45 -04:00
Charlie Marsh
ca6551eb37 Remove misc. unnecessary statements 2022-09-29 18:45:10 -04:00
Charlie Marsh
43a4f5749e Create CODE_OF_CONDUCT.md (#287) 2022-09-29 16:59:17 -04:00
Charlie Marsh
6fef4db433 Bump version to 0.0.48 2022-09-29 16:40:01 -04:00
Charlie Marsh
7470d6832f Add pattern matching limitation to README.md 2022-09-29 16:39:25 -04:00
Nikita Sobolev
63ba0bfeef Adds flake8-builtins (#284) 2022-09-29 16:37:43 -04:00
Anders Kaseorg
91666fcaf6 Don’t follow directory symlinks found while walking (#280) 2022-09-29 15:10:25 -04:00
Heyward Fann
643e27221d chore: fix eslint fix link (#281) 2022-09-29 07:15:07 -04:00
Charlie Marsh
c7349b69c1 Bump version to 0.0.47 2022-09-28 22:30:48 -04:00
Charlie Marsh
7f84753f3c Improve rendering of --show-settings 2022-09-28 22:30:20 -04:00
Charlie Marsh
e2ec62cf33 Misc. follow-up changes to #272 (#278) 2022-09-28 22:15:58 -04:00
Charlie Marsh
1d5592d937 Use take-while to terminate on parse errors (#279) 2022-09-28 22:06:35 -04:00
Anders Kaseorg
886def13bd Upgrade to clap 4 (#272) 2022-09-28 17:11:57 -04:00
Charlie Marsh
949e4d4077 Bump version to 0.0.46 2022-09-24 13:10:10 -04:00
Charlie Marsh
c8cb2eead2 Remove README note about noqa patterns 2022-09-24 13:09:45 -04:00
Seamooo
02ae494a0e Enable per-file ignores (#261) 2022-09-24 13:02:34 -04:00
Harutaka Kawamura
dce86e065b Make unused variable pattern configurable (#265) 2022-09-24 10:43:39 -04:00
Harutaka Kawamura
d77979429c Print warning and error messages in stderr (#267) 2022-09-24 09:27:35 -04:00
Adrian Garcia Badaracco
a3a15d2eb2 error invalid pyproject.toml configs (#264) 2022-09-23 21:16:07 -04:00
Charlie Marsh
5af95428ff Tweak import 2022-09-23 18:53:57 -04:00
Harutaka Kawamura
6338cad4e6 Remove python 3.6 classifier (#260) 2022-09-22 20:38:09 -04:00
Harutaka Kawamura
485881877f Include error code and message in JSON output (#259) 2022-09-22 20:29:21 -04:00
Charlie Marsh
b8f517c70e Bump version to 0.0.45 2022-09-22 14:11:09 -04:00
Charlie Marsh
9f601c2abd Document noqa workflows 2022-09-22 14:10:02 -04:00
Charlie Marsh
c0ce0b0c48 Enable automatic noqa insertion (#256) 2022-09-22 13:59:06 -04:00
Charlie Marsh
e5b16973a9 Enable autofix for M001 (#255) 2022-09-22 13:21:03 -04:00
Charlie Marsh
de9ceb2fe1 Only enforce multi-line noqa directives for strings (#258) 2022-09-22 13:09:02 -04:00
Charlie Marsh
38b19b78b7 Enable noqa directives on logical lines (#257) 2022-09-22 12:56:15 -04:00
Charlie Marsh
7043e15b57 Move noqa to a separate module 2022-09-22 09:04:54 -04:00
Charlie Marsh
9594079235 Add --extend-select and --extend-ignore (#254) 2022-09-21 19:56:43 -04:00
Charlie Marsh
732f208e47 Add a lint rule to enforce noqa validity (#253) 2022-09-21 19:56:38 -04:00
Charlie Marsh
32e62d9209 Use specific version tags 2022-09-21 15:11:53 -04:00
Charlie Marsh
d9e4b0cdc1 Implement --show-settings and --show-files (#246) 2022-09-21 15:08:50 -04:00
Charlie Marsh
36fcfad56a Remove empty comment 2022-09-21 13:44:49 -04:00
Charlie Marsh
65d29d9734 Adjust line numbers when reporting rules in f-strings (#244) 2022-09-21 13:42:58 -04:00
Charlie Marsh
1e171ce0e8 Bump version to 0.0.44 2022-09-21 12:25:14 -04:00
Charlie Marsh
2bdc500c61 Re-run cargo insta 2022-09-21 12:24:46 -04:00
Charlie Marsh
f453e429b6 Add a note on parity 2022-09-21 12:24:04 -04:00
Charlie Marsh
73874f4788 Remove proof-of-concept caveat 2022-09-21 12:18:01 -04:00
Charlie Marsh
8846dcdf6a Update README 2022-09-21 12:17:41 -04:00
Charlie Marsh
d827e6e36a Implement F405 (#243) 2022-09-21 12:13:40 -04:00
Harutaka Kawamura
71d9b2ac5f Implement F402 (#221) 2022-09-21 11:12:55 -04:00
Anders Kaseorg
401b53cc45 Handle filesystem errors more consistently (#240) 2022-09-20 23:22:01 -04:00
Anders Kaseorg
aa9c1e255c Simplify check_path type (#239) 2022-09-20 21:11:42 -04:00
Anders Kaseorg
f7fc702b2c Include specified files, even if they lack a .py[i] extension (#238) 2022-09-20 20:53:52 -04:00
Anders Kaseorg
50ca0d7d0a Correctly display the location of parse errors (#237) 2022-09-20 20:53:22 -04:00
Anders Kaseorg
65e0284698 Suppress “Found 0 error(s)” message (#236) 2022-09-20 19:32:39 -04:00
Charlie Marsh
e4f571ea61 Bump version to 0.0.43 2022-09-20 12:26:49 -04:00
Charlie Marsh
4ed88dd245 Follow-up fixes to path absolution (#235) 2022-09-20 12:26:32 -04:00
Charlie Marsh
09b926fd59 Optimize imports 2022-09-20 09:10:39 -04:00
Charlie Marsh
a4869e4974 Update benchmark in README 2022-09-20 07:06:12 -06:00
Charlie Marsh
f53c4fc221 Bump version to 0.0.42 2022-09-19 21:14:17 -06:00
Charlie Marsh
3892a49a97 Bump version to 0.0.41 2022-09-19 21:09:33 -06:00
Charlie Marsh
27cc7e236c Use a separate repo for pre-commit (#229) 2022-09-19 21:06:39 -06:00
Charlie Marsh
fa0954fe47 Treat relative excludes as relative to project root (#228) 2022-09-19 20:45:02 -06:00
Charlie Marsh
a0b50d7ebc Use absolute paths for exclusion matching (#213) 2022-09-19 20:32:31 -06:00
Charlie Marsh
afe7a04211 Ignore F841 violations when locals() is in scope (#226) 2022-09-19 20:13:55 -06:00
Charlie Marsh
14806c62ca Reduce number of sites for new check definitions (#227) 2022-09-19 20:13:46 -06:00
Suguru Yamamoto
0d0c8730fa fix: Use UTF-32 char count for line length (#223) (#224) 2022-09-18 10:45:41 -06:00
Harutaka Kawamura
cf6a23b83c Add --version flag (#222) 2022-09-18 09:15:15 -06:00
Anders Kaseorg
9e0daac561 Enable F404 by default (#219) 2022-09-18 09:13:23 -06:00
Anders Kaseorg
f2fd7335ce Use a platform-appropriate location for user configuration (#215) 2022-09-17 13:29:17 -06:00
Anders Kaseorg
b8f878df5e Find user configuration even if there’s no project directory (#216) 2022-09-16 21:47:15 -06:00
Anders Kaseorg
9bdb922c75 Detect multi-target assignment as unpacking if *any* target is unpacking (#217) 2022-09-16 21:45:44 -06:00
Anders Kaseorg
edecc1bba6 Fix find_project_root with relative paths (#214) 2022-09-16 18:04:35 -06:00
Charlie Marsh
8e903153f6 Update README to include more badges 2022-09-16 12:18:03 -06:00
Charlie Marsh
3937885f37 Bump version to 0.0.40 2022-09-16 04:57:21 -04:00
Charlie Marsh
24de97d951 Create cache directory prior to writing .gitignore 2022-09-16 04:56:58 -04:00
93 changed files with 4340 additions and 1135 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# Check http://editorconfig.org for more information
# This is the main config file for this project:
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
indent_style = space
insert_final_newline = true
indent_size = 2
[*.{rs,py}]
indent_size = 4

View File

@@ -1,5 +1,10 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.39
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
hooks:
- id: lint
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.10.1
hooks:
- id: validate-pyproject

View File

@@ -1,7 +0,0 @@
- id: lint
name: ruff lint
description: Run ruff to lint Python files.
entry: ruff
language: python
types_or: [python]
pass_filenames: true

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
charlie.r.marsh@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

105
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,105 @@
# Contributing to ruff
Welcome! We're happy to have you here. Thank you in advance for your contribution to ruff.
## The basics
ruff welcomes contributions in the form of Pull Requests. 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 submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
your proposed change.
### Prerequisites
ruff is written in Rust (1.63.0). You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
### Development
After cloning the repository, run ruff locally with:
```shell
cargo run resources/test/fixtures --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes
both the lint and test validation checks:
```shell
cargo fmt # Auto-formatting...
cargo clippy # Linting...
cargo test # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
will save you time and expedite the merge process.
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
### Example: Adding a new lint rule
There are three phases to adding a new lint rule:
1. Define the rule in `src/checks.rs`.
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks)
or `src/check_lines.rs` (for text-based checks).
3. Add a test fixture.
To define the rule, open up `src/checks.rs`. You'll need to define both a `CheckCode` and
`CheckKind`. As an example, you can grep for `E402` and `ModuleImportNotAtTopOfFile`, and follow the
pattern implemented therein.
To trigger the rule, you'll likely want to augment the logic in `src/check_ast.rs`, which defines
the Python AST visitor, responsible for iterating over the abstract syntax tree and collecting
lint-rule violations as it goes. Grep for the `Check::new` invocations to understand how other,
similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisified with the output,
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
`src/linter.rs`, like so:
```rust
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings::for_rule(CheckCode::E402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
```
Then, run `cargo test`. Your test will fail, but you'll be prompted to follow-up with
`cargo insta review`. Accept the generated snapshot, then commit the snapshot file alongside the
rest of your changes.
### Example: Adding a new configuration option
ruff's user-facing settings live in two places: first, the command-line options defined with
[clap](https://docs.rs/clap/latest/clap/) via the `Cli` struct in `src/main.rs`; and second, the
`Config` struct defined `src/pyproject.rs`, which is responsible for extracting user-defined
settings from a `pyproject.toml` file.
Ultimately, these two sources of configuration are merged into the `Settings` struct defined
in `src/settings.rs`, which is then threaded through the codebase.
To add a new configuration option, you'll likely want to _both_ add a CLI option to `src/main.rs`
_and_ a `pyproject.toml` parameter to `src/pyproject.rs`. If you want to pattern-match against an
existing example, grep for `dummy_variable_rgx`, which defines a regular expression to match against
acceptable unused variables (e.g., `_`).
## Release process
As of now, ruff has an ad hoc release process: releases are cut with high frequency via GitHub
Actions, which automatically generates the appropriate wheels across architectures and publishes
them to [PyPI](https://pypi.org/project/ruff/).
ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).

117
Cargo.lock generated
View File

@@ -37,6 +37,12 @@ dependencies = [
"libc",
]
[[package]]
name = "annotate-snippets"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7"
[[package]]
name = "anyhow"
version = "1.0.60"
@@ -359,6 +365,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chic"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c"
dependencies = [
"annotate-snippets",
]
[[package]]
name = "chrono"
version = "0.4.21"
@@ -382,26 +397,24 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "clap"
version = "3.2.16"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9"
checksum = "dd03107d0f87139c1774a15f3db2165b0652b5460c58c27e561f89c20c599eaf"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.15"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
checksum = "ca689d7434ce44517a12a89456b2be4d1ea1cafcd8f581978c03d45f5a5c12a7"
dependencies = [
"heck",
"proc-macro-error",
@@ -412,9 +425,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.2.4"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
dependencies = [
"os_str_bytes",
]
@@ -1064,9 +1077,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.10.3"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
@@ -1155,6 +1168,30 @@ version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=32a044c127668df44582f85699358e67803b0d73#32a044c127668df44582f85699358e67803b0d73"
dependencies = [
"chic",
"itertools",
"libcst_derive",
"once_cell",
"paste",
"peg",
"regex",
"thiserror",
]
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=32a044c127668df44582f85699358e67803b0d73#32a044c127668df44582f85699358e67803b0d73"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -1381,9 +1418,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.13.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "opaque-debug"
@@ -1432,6 +1469,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "paste"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
[[package]]
name = "path-absolutize"
version = "3.0.13"
@@ -1450,6 +1493,33 @@ dependencies = [
"once_cell",
]
[[package]]
name = "peg"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554"
dependencies = [
"peg-macros",
"peg-runtime",
]
[[package]]
name = "peg-macros"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b"
dependencies = [
"peg-runtime",
"proc-macro2",
"quote",
]
[[package]]
name = "peg-runtime"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -1801,7 +1871,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.39"
version = "0.0.50"
dependencies = [
"anyhow",
"bincode",
@@ -1817,6 +1887,7 @@ dependencies = [
"glob",
"insta",
"itertools",
"libcst",
"log",
"notify",
"once_cell",
@@ -1846,7 +1917,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -1855,7 +1926,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
dependencies = [
"bincode",
"bitflags",
@@ -1872,7 +1943,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=7d21c6923a506e79cc041708d83cef925efd33f4#7d21c6923a506e79cc041708d83cef925efd33f4"
source = "git+https://github.com/charliermarsh/RustPython.git?rev=4f457893efc381ad5c432576b24bcc7e4a08c641#4f457893efc381ad5c432576b24bcc7e4a08c641"
dependencies = [
"ahash",
"anyhow",
@@ -2187,26 +2258,20 @@ dependencies = [
"phf_codegen 0.8.0",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.32"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.32"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.39"
version = "0.0.50"
edition = "2021"
[lib]
@@ -11,22 +11,23 @@ anyhow = { version = "1.0.60" }
bincode = { version = "1.3.3" }
cacache = { version = "10.0.1" }
chrono = { version = "0.4.21" }
clap = { version = "3.2.16", features = ["derive"] }
clap = { version = "4.0.1", features = ["derive"] }
clearscreen = { version = "1.0.10" }
colored = { version = "2.0.0" }
common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = "0.3.0"
itertools = "0.10.3"
glob = { version = "0.3.0" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "32a044c127668df44582f85699358e67803b0d73" }
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
path-absolutize = "3.0.13"
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "4f457893efc381ad5c432576b24bcc7e4a08c641" }
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.83" }
toml = { version = "0.5.9" }

367
README.md
View File

@@ -1,7 +1,9 @@
# ruff
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![PyPI version](https://badge.fury.io/py/ruff.svg)](https://badge.fury.io/py/ruff)
An extremely fast Python linter, written in Rust.
@@ -18,11 +20,9 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired `--fix` support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)-inspired `--fix` support
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support
_ruff is a proof-of-concept and not yet intended for production use. It supports only a small subset
of the Flake8 rules, and may crash on your codebase._
- ⚖️ [Near-complete parity](#Parity-with-Flake8) with the built-in Flake8 rule set
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -52,14 +52,14 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
ruff path/to/code/ --watch
```
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.39
hooks:
- id: lint
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.48
hooks:
- id: lint
```
## Configuration
@@ -80,87 +80,139 @@ select = [
Alternatively, on the command-line:
```shell
ruff path/to/code/ --select F401 F403
ruff path/to/code/ --select F401 --select F403
```
See `ruff --help` for more:
```shell
ruff (v0.0.39)
An extremely fast Python linter.
ruff: An extremely fast Python linter.
USAGE:
ruff [OPTIONS] <FILES>...
Usage: ruff [OPTIONS] <FILES>...
ARGS:
<FILES>...
Arguments:
<FILES>...
OPTIONS:
-e, --exit-zero
Exit with status code "0", even upon detecting errors
--exclude <EXCLUDE>...
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>...
Like --exclude, but adds additional files and directories on top of the excluded ones
-f, --fix
Attempt to automatically fix lint errors
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text,
json]
-h, --help
Print help information
--ignore <IGNORE>...
List of error codes to ignore
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--select <SELECT>...
List of error codes to enable
-v, --verbose
Enable verbose logging
-w, --watch
Run in watch mode by re-running whenever files change
Options:
-v, --verbose
Enable verbose logging
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
-e, --exit-zero
Exit with status code "0", even upon detecting errors
-w, --watch
Run in watch mode by re-running whenever files change
-f, --fix
Attempt to automatically fix lint errors
-n, --no-cache
Disable cache reads
--select <SELECT>
List of error codes to enable
--extend-select <EXTEND_SELECT>
Like --select, but adds additional error codes on top of the selected ones
--ignore <IGNORE>
List of error codes to ignore
--extend-ignore <EXTEND_IGNORE>
Like --ignore, but adds additional error codes on top of the ignored ones
--exclude <EXCLUDE>
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>
Like --exclude, but adds additional files and directories on top of the excluded ones
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text, json]
--show-files
See the files ruff will be run against with the current settings
--show-settings
See ruff's settings
--add-noqa
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
-h, --help
Print help information
-V, --version
Print version information
```
### Excluding files
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `./directory/foo.py` (to exclude that specific file) or `./directory/*.py`
(to exclude any Python files in `./directory`). Note that these paths are relative to the
directory from which you execute `ruff`, and _not_ the directory of the `pyproject.toml`.
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
```python
# Ignore F841.
x = 1 # noqa: F841
# Ignore E741 and F841.
i = 1 # noqa: E741, F841
# Ignore _all_ errors.
x = 1 # noqa
```
Note that, for multi-line strings, the `noqa` directive should come at the end of the string, and
will apply to the entire body, like so:
```python
"""Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
```
ruff supports several (experimental) workflows to aid in `noqa` management.
First, ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` 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 M001 --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
add `noqa` directives to all failing lines, with the appropriate error codes.**
### Compatibility with Black
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
compatible out-of-the-box as long as the `line-length` setting is consistent between the two.
ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
the `line-length` setting is consistent between the two.
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
lint rules that are obviated by Black (e.g., stylistic rules).
stylistic lint rules that are obviated by autoformatting.
### Parity with Flake8
ruff's goal is to achieve feature-parity with Flake8 when used (1) without any plugins,
(2) alongside Black, and (3) on Python 3 code. (Using Black obviates the need for many of Flake8's
stylistic checks; limiting to Python 3 obviates the need for certain compatibility checks.)
ruff's goal is to achieve feature-parity with Flake8 when used (1) without plugins, (2) alongside
Black, and (3) on Python 3 code.
Under those conditions, Flake8 implements about 60 rules, give or take. At time of writing, ruff
implements 42 rules. (Note that these 42 rules likely cover a disproportionate share of errors:
unused imports, undefined variables, etc.)
The unimplemented rules are tracked in #170, and include:
- 14 rules related to string `.format` calls.
- 4 logical rules.
- 1 rule related to parsing.
**Under those conditions, ruff implements 44 out of 60 rules.** (ruff is missing: 14 rules related
to string `.format` calls, 1 rule related to docstring parsing, and 1 rule related to redefined
variables.)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
2. Flake8 has a plugin architecture and supports writing custom lint rules.
3. ruff does not yet support parenthesized context managers.
1. Flake8 has a plugin architecture and supports writing custom lint rules.
2. ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
## Rules
@@ -178,11 +230,13 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F
| E741 | AmbiguousVariableName | ambiguous variable name '...' |
| E742 | AmbiguousClassName | ambiguous class name '...' |
| E743 | AmbiguousFunctionName | ambiguous function name '...' |
| E902 | IOError | No such file or directory: `...` |
| E902 | IOError | ... |
| E999 | SyntaxError | SyntaxError: ... |
| F401 | UnusedImport | `...` imported but unused |
| F403 | ImportStarUsage | `from ... import *` used; unable to detect undefined names |
| F402 | ImportShadowedByLoopVar | import '...' from line 1 shadowed by loop variable |
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names |
| F404 | LateFutureImport | from __future__ imports must occur at the beginning of the file |
| F405 | ImportStarUsage | '...' may be undefined, or defined from star imports: ... |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level |
| F407 | FutureFeatureNotDefined | future feature '...' is not defined |
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
@@ -206,8 +260,51 @@ Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis F
| F831 | DuplicateArgumentName | Duplicate argument name in function definition |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin |
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin |
| A003 | BuiltinAttributeShadowing | class attribute `...` is shadowing a python builtin |
| SPR001 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` |
| R001 | UselessObjectInheritance | Class `...` inherits from object |
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
| M001 | UnusedNOQA | Unused `noqa` directive |
## Integrations
### PyCharm
ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
in PyCharm. Open the Preferences pane, then navigate to "Tools", then "External Tools". From there,
add a new tool with the following configuration:
![Install ruff as an External Tool](https://user-images.githubusercontent.com/1309177/193155720-336e43f0-1a8d-46b4-bc12-e60f9ae01f7e.png)
ruff should then appear as a runnable action:
![ruff as a runnable action](https://user-images.githubusercontent.com/1309177/193156026-732b0aaf-3dd9-4549-9b4d-2de6d2168a33.png)
### GitHub Actions
GitHub Actions has everything you need to run ruff out-of-the-box:
```yaml
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Run ruff
run: ruff .
```
## Development
@@ -243,29 +340,29 @@ Add this `pyproject.toml` to the CPython directory:
```toml
[tool.ruff]
line-length = 88
exclude = [
"./resources/test/cpython/Lib/lib2to3/tests/data/bom.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/crlf.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/different_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/false_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/py2_test_grammar.py",
"./resources/test/cpython/Lib/test/bad_coding2.py",
"./resources/test/cpython/Lib/test/badsyntax_3131.py",
"./resources/test/cpython/Lib/test/badsyntax_pep3120.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_iso_8859_1.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_koi8_r.py",
"./resources/test/cpython/Lib/test/test_fstring.py",
"./resources/test/cpython/Lib/test/test_grammar.py",
"./resources/test/cpython/Lib/test/test_importlib/test_util.py",
"./resources/test/cpython/Lib/test/test_named_expressions.py",
"./resources/test/cpython/Lib/test/test_patma.py",
"./resources/test/cpython/Lib/test/test_source_encoding.py",
"./resources/test/cpython/Tools/c-analyzer/c_parser/parser/_delim.py",
"./resources/test/cpython/Tools/i18n/pygettext.py",
"./resources/test/cpython/Tools/test2to3/maintest.py",
"./resources/test/cpython/Tools/test2to3/setup.py",
"./resources/test/cpython/Tools/test2to3/test/test_foo.py",
"./resources/test/cpython/Tools/test2to3/test2to3/hello.py",
extend-exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/test_fstring.py",
"Lib/test/test_grammar.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_source_encoding.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
"Tools/test2to3/setup.py",
"Tools/test2to3/test/test_foo.py",
"Tools/test2to3/test2to3/hello.py",
]
```
@@ -274,17 +371,21 @@ Next, to benchmark the release build:
```shell
cargo build --release
hyperfine --ignore-failure --warmup 1 \
hyperfine --ignore-failure --warmup 10 --runs 100 \
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
"./target/release/ruff ./resources/test/cpython/"
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 353.6 ms ± 7.6 ms [User: 2868.8 ms, System: 171.5 ms]
Range (min … max): 344.4 ms … 367.3 ms 10 runs
Time (mean ± σ): 297.4 ms ± 4.9 ms [User: 2460.0 ms, System: 67.2 ms]
Range (min … max): 287.7 ms … 312.1 ms 100 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./resources/test/cpython/
Time (mean ± σ): 59.6 ms ± 2.5 ms [User: 36.4 ms, System: 345.6 ms]
Range (min … max): 55.9 ms … 67.0 ms 48 runs
Time (mean ± σ): 79.6 ms ± 7.3 ms [User: 59.7 ms, System: 356.1 ms]
Range (min … max): 62.4 ms … 111.2 ms 100 runs
Warning: Ignoring non-zero exit code.
```
To benchmark against the ecosystem's existing tools:
@@ -296,11 +397,8 @@ hyperfine --ignore-failure --warmup 5 \
"pyflakes resources/test/cpython" \
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
"pycodestyle resources/test/cpython" \
"pycodestyle --select E501 resources/test/cpython" \
"flake8 resources/test/cpython" \
"flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython" \
"python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501"
"python -m scripts.run_flake8 resources/test/cpython"
```
In order, these evaluate:
@@ -310,68 +408,65 @@ In order, these evaluate:
- PyFlakes
- autoflake
- pycodestyle
- pycodestyle, limited to the checks supported by ruff
- Flake8
- Flake8, limited to the checks supported by ruff
- Flake8, with a hack to enable multiprocessing on macOS
- Flake8, with a hack to enable multiprocessing on macOS, limited to the checks supported by ruff
(You can `poetry install` from `./scripts` to create a working environment for the above.)
```shell
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
Time (mean ± σ): 469.3 ms ± 16.3 ms [User: 2663.0 ms, System: 972.5 ms]
Range (min … max): 445.2 ms … 494.8 ms 10 runs
Time (mean ± σ): 297.9 ms ± 7.0 ms [User: 2436.6 ms, System: 65.9 ms]
Range (min … max): 289.9 ms … 314.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: pylint --recursive=y resources/test/cpython/
Time (mean ± σ): 27.211 s ± 0.097 s [User: 26.405 s, System: 0.799 s]
Range (min … max): 27.056 s … 27.349 s 10 runs
Time (mean ± σ): 37.634 s ± 0.225 s [User: 36.728 s, System: 0.853 s]
Range (min … max): 37.201 s … 38.106 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 3: pyflakes resources/test/cpython
Time (mean ± σ): 27.309 s ± 0.033 s [User: 27.137 s, System: 0.169 s]
Range (min … max): 27.267 s … 27.372 s 10 runs
Time (mean ± σ): 40.950 s ± 0.449 s [User: 40.688 s, System: 0.229 s]
Range (min … max): 40.348 s … 41.671 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
Time (mean ± σ): 8.027 s ± 0.024 s [User: 74.255 s, System: 0.953 s]
Range (min … max): 7.969 s … 8.052 s 10 runs
Time (mean ± σ): 11.562 s ± 0.160 s [User: 107.022 s, System: 1.143 s]
Range (min … max): 11.417 s … 11.917 s 10 runs
Benchmark 5: pycodestyle resources/test/cpython
Time (mean ± σ): 41.666 s ± 0.266 s [User: 41.531 s, System: 0.132 s]
Range (min … max): 41.295 s … 41.980 s 10 runs
Time (mean ± σ): 67.428 s ± 0.985 s [User: 67.199 s, System: 0.203 s]
Range (min … max): 65.313 s … 68.496 s 10 runs
Benchmark 6: pycodestyle --select E501 resources/test/cpython
Time (mean ± σ): 14.547 s ± 0.077 s [User: 14.466 s, System: 0.079 s]
Range (min … max): 14.429 s … 14.695 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 7: flake8 resources/test/cpython
Time (mean ± σ): 75.700 s ± 0.152 s [User: 75.254 s, System: 0.440 s]
Range (min … max): 75.513 s … 76.014 s 10 runs
Benchmark 6: flake8 resources/test/cpython
Time (mean ± σ): 116.099 s ± 1.178 s [User: 115.217 s, System: 0.845 s]
Range (min … max): 114.180 s … 117.724 s 10 runs
Benchmark 8: flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython
Time (mean ± σ): 75.122 s ± 0.532 s [User: 74.677 s, System: 0.440 s]
Range (min … max): 74.130 s … 75.606 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 9: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 12.794 s ± 0.147 s [User: 90.792 s, System: 0.738 s]
Range (min … max): 12.606 s … 13.030 s 10 runs
Benchmark 10: python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501
Time (mean ± σ): 12.487 s ± 0.118 s [User: 90.052 s, System: 0.714 s]
Range (min … max): 12.265 s … 12.665 s 10 runs
Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
Time (mean ± σ): 20.477 s ± 0.349 s [User: 142.372 s, System: 1.504 s]
Range (min … max): 20.107 s … 21.183 s 10 runs
Summary
'./target/release/ruff ./resources/test/cpython/ --no-cache' ran
17.10 ± 0.60 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
26.60 ± 0.96 times faster than 'python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501'
27.26 ± 1.00 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
30.99 ± 1.09 times faster than 'pycodestyle --select E501 resources/test/cpython'
57.98 ± 2.03 times faster than 'pylint --recursive=y resources/test/cpython/'
58.19 ± 2.02 times faster than 'pyflakes resources/test/cpython'
88.77 ± 3.14 times faster than 'pycodestyle resources/test/cpython'
160.06 ± 5.68 times faster than 'flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython'
161.29 ± 5.61 times faster than 'flake8 resources/test/cpython'
38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
```
## License
MIT
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTORS.md).

View File

@@ -1,56 +1,14 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckKind, RejectedCmpop};
use ruff::checks::{CheckCode, ALL_CHECK_CODES};
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AmbiguousClassName("...".to_string()),
CheckKind::AmbiguousFunctionName("...".to_string()),
CheckKind::AmbiguousVariableName("...".to_string()),
CheckKind::AssertTuple,
CheckKind::BreakOutsideLoop,
CheckKind::ContinueOutsideLoop,
CheckKind::DefaultExceptNotLast,
CheckKind::DoNotAssignLambda,
CheckKind::DoNotUseBareExcept,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckKind::IOError("...".to_string()),
CheckKind::IfTuple,
CheckKind::ImportStarNotPermitted("...".to_string()),
CheckKind::ImportStarUsage("...".to_string()),
CheckKind::InvalidPrintSyntax,
CheckKind::IsLiteral,
CheckKind::LateFutureImport,
CheckKind::LineTooLong(89, 88),
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::MultiValueRepeatedKeyLiteral,
CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckKind::NoAssertEquals,
CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckKind::NotInTest,
CheckKind::NotIsTest,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::SyntaxError("...".to_string()),
CheckKind::TooManyExpressionsInStarredAssignment,
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckKind::TwoStarredExpressions,
CheckKind::TypeComparison,
CheckKind::UndefinedExport("...".to_string()),
CheckKind::UndefinedLocal("...".to_string()),
CheckKind::UndefinedName("...".to_string()),
CheckKind::UnusedImport("...".to_string()),
CheckKind::UnusedVariable("...".to_string()),
CheckKind::UselessObjectInheritance("...".to_string()),
CheckKind::YieldOutsideFunction,
];
check_kinds.sort_by_key(|check_kind| check_kind.code());
let mut check_codes: Vec<CheckCode> = ALL_CHECK_CODES.to_vec();
check_codes.sort();
println!("| Code | Name | Message |");
println!("| ---- | ----- | ------- |");
for check_kind in check_kinds {
for check_code in check_codes {
let check_kind = check_code.kind();
println!(
"| {} | {} | {} |",
check_kind.code().as_str(),

View File

@@ -2,14 +2,14 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::Parser;
use rustpython_parser::parser;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
#[arg(required = true)]
file: PathBuf,
}

View File

@@ -2,14 +2,14 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::Parser;
use rustpython_parser::lexer;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
#[arg(required = true)]
file: PathBuf,
}

View File

@@ -8,7 +8,6 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",

27
resources/test/fixtures/A001.py vendored Normal file
View File

@@ -0,0 +1,27 @@
import some as sum
from some import other as int
print = 1
copyright: 'annotation' = 2
(complex := 3)
float = object = 4
min, max = 5, 6
def bytes():
pass
class slice:
pass
try:
...
except ImportError as ValueError:
...
for memoryview, *bytearray in []:
pass
with open('file') as str, open('file2') as (all, any):
pass
[0 for sum in ()]

9
resources/test/fixtures/A002.py vendored Normal file
View File

@@ -0,0 +1,9 @@
def func1(str, /, type, *complex, Exception, **getattr):
pass
async def func2(bytes):
pass
map([], lambda float: ...)

8
resources/test/fixtures/A003.py vendored Normal file
View File

@@ -0,0 +1,8 @@
class MyClass:
ImportError = 4
def __init__(self):
self.float = 5 # is fine
def str(self):
pass

View File

@@ -1,4 +1,8 @@
"""Top-level docstring."""
__all__ = ["y"]
__version__: str = "0.1.0"
import a
try:

View File

@@ -4,3 +4,13 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
_ = "---------------------------------------------------------------------------AAAAAAA"
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜"

9
resources/test/fixtures/F402.py vendored Normal file
View File

@@ -0,0 +1,9 @@
import os
import os.path as path
for os in range(3):
pass
for path in range(3):
pass

11
resources/test/fixtures/F405.py vendored Normal file
View File

@@ -0,0 +1,11 @@
from mymodule import *
def print_name():
print(name)
def print_name(name):
print(name)
__all__ = ['a']

View File

@@ -82,3 +82,10 @@ class Ticket:
def update_tomato():
print(TOMATO)
TOMATO = "cherry tomato"
A = f'{B}'
A = (
f'B'
f'{B}'
)

View File

@@ -10,15 +10,28 @@ except ValueError as e:
print(e)
def f():
def f1():
x = 1
y = 2
z = x + y
def g():
def f2():
foo = (1, 2)
(a, b) = (1, 2)
bar = (1, 2)
(c, d) = bar
(x, y) = baz = bar
def f3():
locals()
x = 1
def f4():
_ = 1
__ = 1
_discarded = 1

57
resources/test/fixtures/M001.py vendored Normal file
View File

@@ -0,0 +1,57 @@
def f() -> None:
# Valid
a = 1 # noqa
# Valid
b = 2 # noqa: F841
# Invalid
c = 1 # noqa
print(c)
# Invalid
d = 1 # noqa: E501
# Invalid
d = 1 # noqa: F841, E501
# Valid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501
# Valid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""" # noqa: E501, F841
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa: E501
# Invalid
_ = """Lorem ipsum dolor sit amet.
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
""" # noqa

22
resources/test/fixtures/SPR001.py vendored Normal file
View File

@@ -0,0 +1,22 @@
class Parent:
def method(self):
pass
def wrong(self):
pass
class Child(Parent):
def method(self):
parent = super() # ok
super().method() # ok
Parent.method(self) # ok
Parent.super(1, 2) # ok
def wrong(self):
parent = super(Child, self) # wrong
super(Child, self).method # wrong
super(
Child,
self,
).method() # wrong

View File

@@ -3,49 +3,5 @@ line-length = 88
extend-exclude = [
"excluded.py",
"migrations",
"./resources/test/fixtures/directory/also_excluded.py",
]
select = [
"E402",
"E501",
"E711",
"E712",
"E713",
"E714",
"E721",
"E722",
"E731",
"E741",
"E742",
"E743",
"E902",
"E999",
"F401",
"F403",
"F404",
"F406",
"F407",
"F541",
"F601",
"F602",
"F621",
"F622",
"F631",
"F632",
"F633",
"F634",
"F701",
"F702",
"F704",
"F706",
"F707",
"F722",
"F821",
"F822",
"F823",
"F831",
"F841",
"F901",
"R001",
"R002",
"directory/also_excluded.py",
]

View File

@@ -1,18 +1,22 @@
use std::collections::BTreeSet;
use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
Location, Stmt, StmtKind, Unaryop,
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, Scope};
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Location) -> Option<Check> {
pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::IfTuple, location));
@@ -22,7 +26,7 @@ pub fn check_if_tuple(test: &Expr, location: Location) -> Option<Check> {
}
/// Check AssertTuple compliance.
pub fn check_assert_tuple(test: &Expr, location: Location) -> Option<Check> {
pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::AssertTuple, location));
@@ -37,6 +41,7 @@ pub fn check_not_tests(
operand: &Expr,
check_not_in: bool,
check_not_is: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -46,12 +51,18 @@ pub fn check_not_tests(
match op {
Cmpop::In => {
if check_not_in {
checks.push(Check::new(CheckKind::NotInTest, operand.location));
checks.push(Check::new(
CheckKind::NotInTest,
locator.locate_check(Range::from_located(operand)),
));
}
}
Cmpop::Is => {
if check_not_is {
checks.push(Check::new(CheckKind::NotIsTest, operand.location));
checks.push(Check::new(
CheckKind::NotIsTest,
locator.locate_check(Range::from_located(operand)),
));
}
}
_ => {}
@@ -64,21 +75,31 @@ pub fn check_not_tests(
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
pub fn check_unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: true })
) {
return checks;
}
for (name, binding) in scope.values.iter() {
// TODO(charlie): Ignore if using `locals`.
if binding.used.is_none()
&& name != "_"
&& matches!(binding.kind, BindingKind::Assignment)
&& !dummy_variable_rgx.is_match(name)
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
&& matches!(binding.kind, BindingKind::Assignment)
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
binding.location,
locator.locate_check(binding.location),
));
}
}
@@ -87,7 +108,7 @@ pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
}
/// Check DoNotAssignLambda compliance.
pub fn check_do_not_assign_lambda(value: &Expr, location: Location) -> Option<Check> {
pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
@@ -100,7 +121,7 @@ fn is_ambiguous_name(name: &str) -> bool {
}
/// Check AmbiguousVariableName compliance.
pub fn check_ambiguous_variable_name(name: &str, location: Location) -> Option<Check> {
pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
@@ -112,7 +133,7 @@ pub fn check_ambiguous_variable_name(name: &str, location: Location) -> Option<C
}
/// Check AmbiguousClassName compliance.
pub fn check_ambiguous_class_name(name: &str, location: Location) -> Option<Check> {
pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousClassName(name.to_string()),
@@ -124,7 +145,7 @@ pub fn check_ambiguous_class_name(name: &str, location: Location) -> Option<Chec
}
/// Check AmbiguousFunctionName compliance.
pub fn check_ambiguous_function_name(name: &str, location: Location) -> Option<Check> {
pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousFunctionName(name.to_string()),
@@ -156,7 +177,7 @@ pub fn check_useless_object_inheritance(
}) => {
let mut check = Check::new(
CheckKind::UselessObjectInheritance(name.to_string()),
expr.location,
Range::from_located(expr),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
@@ -187,7 +208,7 @@ pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Ch
if type_.is_none() && idx < handlers.len() - 1 {
return Some(Check::new(
CheckKind::DefaultExceptNotLast,
handler.location,
Range::from_located(handler),
));
}
}
@@ -201,13 +222,19 @@ pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
if id == "NotImplemented" {
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
return Some(Check::new(
CheckKind::RaiseNotImplemented,
Range::from_located(expr),
));
}
}
}
ExprKind::Name { id, .. } => {
if id == "NotImplemented" {
return Some(Check::new(CheckKind::RaiseNotImplemented, expr.location));
return Some(Check::new(
CheckKind::RaiseNotImplemented,
Range::from_located(expr),
));
}
}
_ => {}
@@ -239,7 +266,10 @@ pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
for arg in all_arguments {
let ident = &arg.node.arg;
if idents.contains(ident.as_str()) {
checks.push(Check::new(CheckKind::DuplicateArgumentName, arg.location));
checks.push(Check::new(
CheckKind::DuplicateArgumentName,
Range::from_located(arg),
));
}
idents.insert(ident);
}
@@ -253,12 +283,16 @@ pub fn check_assert_equals(expr: &Expr, autofix: &fixer::Mode) -> Option<Check>
if attr == "assertEquals" {
if let ExprKind::Name { id, .. } = &value.node {
if id == "self" {
let mut check = Check::new(CheckKind::NoAssertEquals, expr.location);
let mut check =
Check::new(CheckKind::NoAssertEquals, Range::from_located(expr));
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "assertEqual".to_string(),
start: Location::new(expr.location.row(), expr.location.column() + 1),
end: Location::new(
location: Location::new(
expr.location.row(),
expr.location.column() + 1,
),
end_location: Location::new(
expr.location.row(),
expr.location.column() + 1 + "assertEquals".len(),
),
@@ -292,6 +326,7 @@ pub fn check_repeated_keys(
keys: &Vec<Expr>,
check_repeated_literals: bool,
check_repeated_variables: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -306,7 +341,7 @@ pub fn check_repeated_keys(
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyLiteral,
k2.location,
locator.locate_check(Range::from_located(k2)),
))
}
}
@@ -314,7 +349,7 @@ pub fn check_repeated_keys(
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
CheckKind::MultiValueRepeatedKeyVariable((*v2).to_string()),
k2.location,
locator.locate_check(Range::from_located(k2)),
))
}
}
@@ -333,6 +368,7 @@ pub fn check_literal_comparisons(
comparators: &Vec<Expr>,
check_none_comparisons: bool,
check_true_false_comparisons: bool,
locator: &dyn CheckLocator,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -352,13 +388,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
}
@@ -372,13 +408,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
}
@@ -398,13 +434,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::Eq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::NoneComparison(RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
}
@@ -418,13 +454,13 @@ pub fn check_literal_comparisons(
if matches!(op, Cmpop::Eq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::Eq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
if matches!(op, Cmpop::NotEq) {
checks.push(Check::new(
CheckKind::TrueFalseComparison(value, RejectedCmpop::NotEq),
comparator.location,
locator.locate_check(Range::from_located(comparator)),
));
}
}
@@ -461,7 +497,7 @@ pub fn check_is_literal(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Location,
location: Range,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -482,7 +518,7 @@ pub fn check_is_literal(
pub fn check_type_comparison(
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Location,
location: Range,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -521,9 +557,9 @@ pub fn check_type_comparison(
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
elts: &[Expr],
location: Location,
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
location: Range,
) -> Option<Check> {
let mut has_starred: bool = false;
let mut starred_index: Option<usize> = None;
@@ -556,6 +592,7 @@ pub fn check_break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
@@ -582,7 +619,10 @@ pub fn check_break_outside_loop(
}
if !allowed {
Some(Check::new(CheckKind::BreakOutsideLoop, stmt.location))
Some(Check::new(
CheckKind::BreakOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
))
} else {
None
}
@@ -593,6 +633,7 @@ pub fn check_continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
locator: &dyn CheckLocator,
) -> Option<Check> {
let mut allowed: bool = false;
let mut parent = stmt;
@@ -619,8 +660,64 @@ pub fn check_continue_outside_loop(
}
if !allowed {
Some(Check::new(CheckKind::ContinueOutsideLoop, stmt.location))
Some(Check::new(
CheckKind::ContinueOutsideLoop,
locator.locate_check(Range::from_located(stmt)),
))
} else {
None
}
}
// flake8-builtins
pub enum ShadowingType {
Variable,
Argument,
Attribute,
}
/// Check builtin name shadowing
pub fn check_builtin_shadowing(
name: &str,
location: Range,
node_type: ShadowingType,
) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
ShadowingType::Variable => CheckKind::BuiltinVariableShadowing(name.to_string()),
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
},
location,
))
} else {
None
}
}
// flake8-super
/// Check that `super()` has no args
pub fn check_super_args(
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
locator: &mut SourceCodeLocator,
autofix: &fixer::Mode,
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "super" && !args.is_empty() {
let mut check = Check::new(
CheckKind::SuperCallWithParameters,
Range::from_located(expr),
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_super_arguments(locator, expr) {
check.amend(fix);
}
}
return Some(check);
}
}
None
}

View File

@@ -101,11 +101,13 @@ pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
if let StmtKind::Assign { targets, value, .. } = &stmt.node {
for child in targets {
match &child.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => {}
_ => return false,
}
if !targets.iter().any(|child| {
matches!(
child.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}) {
return false;
}
match &value.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => return false,
@@ -132,7 +134,7 @@ impl<'a> SourceCodeLocator<'a> {
}
}
pub fn slice_source_code(&mut self, location: &Location) -> &'a str {
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
@@ -145,4 +147,19 @@ impl<'a> SourceCodeLocator<'a> {
let offset = self.offsets[location.row() - 1] + location.column() - 1;
&self.content[offset..]
}
pub fn slice_source_code_range(&mut self, start: &Location, end: &Location) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
self.offsets.push(offset);
offset += i.len();
offset += 1;
}
self.initialized = true;
}
let start = self.offsets[start.row() - 1] + start.column() - 1;
let end = self.offsets[end.row() - 1] + end.column() - 1;
&self.content[start..end]
}
}

View File

@@ -1,13 +1,17 @@
use rustpython_parser::ast::{Expr, ExprKind, Keyword, Location};
use rustpython_parser::ast::{Expr, ExprKind, Keyword};
fn relocate_keyword(keyword: &mut Keyword, location: Location) {
keyword.location = location;
use crate::ast::types::Range;
fn relocate_keyword(keyword: &mut Keyword, location: Range) {
keyword.location = location.location;
keyword.end_location = location.end_location;
relocate_expr(&mut keyword.node.value, location);
}
/// Change an expression's location (recursively) to match a desired, fixed location.
pub fn relocate_expr(expr: &mut Expr, location: Location) {
expr.location = location;
pub fn relocate_expr(expr: &mut Expr, location: Range) {
expr.location = location.location;
expr.end_location = location.end_location;
match &mut expr.node {
ExprKind::BoolOp { values, .. } => {
for expr in values {

View File

@@ -1,17 +1,37 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_parser::ast::Location;
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Range {
pub location: Location,
pub end_location: Location,
}
impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range {
location: located.location,
end_location: located.end_location,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct FunctionScope {
pub uses_locals: bool,
}
#[derive(Clone, Debug)]
pub enum ScopeKind {
Class,
Function,
Function(FunctionScope),
Generator,
Module,
}
@@ -20,6 +40,7 @@ pub enum ScopeKind {
pub struct Scope {
pub id: usize,
pub kind: ScopeKind,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
@@ -28,6 +49,7 @@ impl Scope {
Scope {
id: id(),
kind,
import_starred: false,
values: BTreeMap::new(),
}
}
@@ -39,6 +61,7 @@ pub enum BindingKind {
Argument,
Assignment,
Binding,
LoopVar,
Builtin,
ClassDefinition,
Definition,
@@ -52,8 +75,12 @@ pub enum BindingKind {
#[derive(Clone, Debug)]
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
/// Tuple of (scope index, location) indicating the scope and location at which the binding was
pub location: Range,
/// Tuple of (scope index, range) indicating the scope and range at which the binding was
/// last used.
pub used: Option<(usize, Location)>,
pub used: Option<(usize, Range)>,
}
pub trait CheckLocator {
fn locate_check(&self, default: Range) -> Range;
}

View File

@@ -45,29 +45,29 @@ fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) ->
for fix in fixes {
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
if last_pos > fix.start {
if last_pos > fix.location {
continue;
}
if fix.start.row() > last_pos.row() {
if fix.location.row() > last_pos.row() {
if last_pos.row() > 0 || last_pos.column() > 0 {
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
output.push('\n');
}
for line in &lines[last_pos.row()..fix.start.row() - 1] {
for line in &lines[last_pos.row()..fix.location.row() - 1] {
output.push_str(line);
output.push('\n');
}
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
output.push_str(&lines[fix.location.row() - 1][..fix.location.column() - 1]);
output.push_str(&fix.content);
} else {
output.push_str(
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.location.column() - 1],
);
output.push_str(&fix.content);
}
last_pos = fix.end;
last_pos = fix.end_location;
fix.applied = true;
}
@@ -106,8 +106,8 @@ mod tests {
fn apply_single_replacement() -> Result<()> {
let mut fixes = vec![Fix {
content: "Bar".to_string(),
start: Location::new(1, 9),
end: Location::new(1, 15),
location: Location::new(1, 9),
end_location: Location::new(1, 15),
applied: false,
}];
let actual = apply_fixes(
@@ -130,8 +130,8 @@ mod tests {
fn apply_single_removal() -> Result<()> {
let mut fixes = vec![Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
location: Location::new(1, 8),
end_location: Location::new(1, 16),
applied: false,
}];
let actual = apply_fixes(
@@ -155,14 +155,14 @@ mod tests {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 17),
location: Location::new(1, 8),
end_location: Location::new(1, 17),
applied: false,
},
Fix {
content: "".to_string(),
start: Location::new(1, 17),
end: Location::new(1, 24),
location: Location::new(1, 17),
end_location: Location::new(1, 24),
applied: false,
},
];
@@ -187,14 +187,14 @@ mod tests {
let mut fixes = vec![
Fix {
content: "".to_string(),
start: Location::new(1, 8),
end: Location::new(1, 16),
location: Location::new(1, 8),
end_location: Location::new(1, 16),
applied: false,
},
Fix {
content: "ignored".to_string(),
start: Location::new(1, 10),
end: Location::new(1, 12),
location: Location::new(1, 10),
end_location: Location::new(1, 12),
applied: false,
},
];

View File

@@ -1,3 +1,4 @@
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
use rustpython_parser::ast::{Expr, Keyword, Location};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
@@ -25,7 +26,7 @@ pub fn remove_class_def_base(
bases: &[Expr],
keywords: &[Keyword],
) -> Option<Fix> {
let content = locator.slice_source_code(stmt_at);
let content = locator.slice_source_code_at(stmt_at);
// Case 1: `object` is the only base.
if bases.len() == 1 && keywords.is_empty() {
@@ -52,8 +53,8 @@ pub fn remove_class_def_base(
return match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
location: start,
end_location: end,
applied: false,
}),
_ => None,
@@ -91,8 +92,8 @@ pub fn remove_class_def_base(
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
location: start,
end_location: end,
applied: false,
}),
_ => None,
@@ -116,11 +117,42 @@ pub fn remove_class_def_base(
match (fix_start, fix_end) {
(Some(start), Some(end)) => Some(Fix {
content: "".to_string(),
start,
end,
location: start,
end_location: end,
applied: false,
}),
_ => None,
}
}
}
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
let contents = locator.slice_source_code_range(&expr.location, &expr.end_location);
let mut tree = match libcst_native::parse_module(contents, None) {
Ok(m) => m,
Err(_) => return None,
};
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
if let Expression::Call(body) = &mut body.value {
body.args = vec![];
body.whitespace_before_args = Default::default();
body.whitespace_after_func = Default::default();
let mut state = Default::default();
tree.codegen(&mut state);
return Some(Fix {
content: state.to_string(),
location: expr.location,
end_location: expr.end_location,
applied: false,
});
}
}
}
None
}

View File

@@ -1,5 +1,5 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::{File, Metadata};
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
@@ -8,6 +8,7 @@ use anyhow::Result;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
@@ -79,7 +80,7 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
autofix.hash(&mut hasher);
format!(
"{}@{}@{}",
path.canonicalize().unwrap().to_string_lossy(),
path.absolutize().unwrap().to_string_lossy(),
VERSION,
hasher.finish()
)
@@ -90,6 +91,7 @@ pub fn init() -> Result<()> {
if gitignore_path.exists() {
return Ok(());
}
create_dir_all(cache_dir())?;
let mut file = File::create(gitignore_path)?;
file.write_all(b"*").map_err(|e| e.into())
}

View File

@@ -1,15 +1,19 @@
use std::ops::Deref;
use std::path::Path;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Location, Operator, Stmt, StmtKind, Suite,
KeywordData, Operator, Stmt, StmtKind, Suite,
};
use rustpython_parser::parser;
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{Binding, BindingKind, Scope, ScopeKind};
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checks, operations, visitor};
use crate::autofix::fixer;
@@ -21,6 +25,8 @@ use crate::settings::Settings;
pub const GLOBAL_SCOPE_INDEX: usize = 0;
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
struct Checker<'a> {
// Input data.
locator: SourceCodeLocator<'a>,
@@ -36,13 +42,13 @@ struct Checker<'a> {
scopes: Vec<Scope>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Location, &'a str)>,
deferred_string_annotations: Vec<(Range, &'a str)>,
deferred_annotations: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_assignments: Vec<usize>,
// Derivative state.
in_f_string: bool,
in_f_string: Option<Range>,
in_annotation: bool,
in_literal: bool,
seen_non_import: bool,
@@ -74,7 +80,7 @@ impl<'a> Checker<'a> {
deferred_functions: vec![],
deferred_lambdas: vec![],
deferred_assignments: vec![],
in_f_string: false,
in_f_string: None,
in_annotation: false,
in_literal: false,
seen_non_import: false,
@@ -101,6 +107,36 @@ fn is_annotated_subscript(expr: &Expr) -> bool {
}
}
fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
// Check whether it's an assignment to a dunder, with or without a type annotation.
// This is what pycodestyle (as of 2.9.1) does.
match node {
StmtKind::Assign {
targets,
value: _,
type_comment: _,
} => {
if targets.len() != 1 {
return false;
}
match &targets[0].node {
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
_ => false,
}
}
StmtKind::AnnAssign {
target,
annotation: _,
value: _,
simple: _,
} => match &target.node {
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
_ => false,
},
_ => false,
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
where
'b: 'a,
@@ -157,10 +193,11 @@ where
self.futures_allowed = false;
}
}
_ => {
node => {
self.futures_allowed = false;
if !self.seen_non_import
&& !is_assignment_to_a_dunder(node)
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
{
self.seen_non_import = true;
@@ -171,7 +208,6 @@ where
// Pre-visit.
match &stmt.node {
StmtKind::Global { names } | StmtKind::Nonlocal { names } => {
// TODO(charlie): Handle doctests.
let global_scope_id = self.scopes[GLOBAL_SCOPE_INDEX].id;
let current_scope =
@@ -184,8 +220,8 @@ where
name.to_string(),
Binding {
kind: BindingKind::Assignment,
used: Some((global_scope_id, stmt.location)),
location: stmt.location,
used: Some((global_scope_id, Range::from_located(stmt))),
location: Range::from_located(stmt),
},
);
}
@@ -193,25 +229,34 @@ where
}
if self.settings.select.contains(&CheckCode::E741) {
self.checks.extend(names.iter().filter_map(|name| {
checks::check_ambiguous_variable_name(name, stmt.location)
}));
let location = self.locate_check(Range::from_located(stmt));
self.checks.extend(
names.iter().filter_map(|name| {
checks::check_ambiguous_variable_name(name, location)
}),
);
}
}
StmtKind::Break => {
if self.settings.select.contains(&CheckCode::F701) {
if let Some(check) =
checks::check_break_outside_loop(stmt, &self.parents, &self.parent_stack)
{
if let Some(check) = checks::check_break_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
self,
) {
self.checks.push(check);
}
}
}
StmtKind::Continue => {
if self.settings.select.contains(&CheckCode::F702) {
if let Some(check) =
checks::check_continue_outside_loop(stmt, &self.parents, &self.parent_stack)
{
if let Some(check) = checks::check_continue_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
self,
) {
self.checks.push(check);
}
}
@@ -231,11 +276,16 @@ where
..
} => {
if self.settings.select.contains(&CheckCode::E743) {
if let Some(check) = checks::check_ambiguous_function_name(name, stmt.location)
{
if let Some(check) = checks::check_ambiguous_function_name(
name,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
}
self.check_builtin_shadowing(name, Range::from_located(stmt), true);
for expr in decorator_list {
self.visit_expr(expr);
}
@@ -278,7 +328,7 @@ where
Binding {
kind: BindingKind::Definition,
used: None,
location: stmt.location,
location: Range::from_located(stmt),
},
);
}
@@ -293,7 +343,7 @@ where
ScopeKind::Class | ScopeKind::Module => {
self.checks.push(Check::new(
CheckKind::ReturnOutsideFunction,
stmt.location,
self.locate_check(Range::from_located(stmt)),
));
}
_ => {}
@@ -325,11 +375,20 @@ where
}
if self.settings.select.contains(&CheckCode::E742) {
if let Some(check) = checks::check_ambiguous_class_name(name, stmt.location) {
if let Some(check) = checks::check_ambiguous_class_name(
name,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
}
self.check_builtin_shadowing(
name,
self.locate_check(Range::from_located(stmt)),
false,
);
for expr in bases {
self.visit_expr(expr)
}
@@ -351,7 +410,7 @@ where
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
self.locate_check(Range::from_located(stmt)),
));
}
@@ -366,10 +425,14 @@ where
alias.node.name.to_string(),
),
used: None,
location: stmt.location,
location: Range::from_located(stmt),
},
)
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
}
self.add_binding(
alias
.node
@@ -385,13 +448,17 @@ where
.unwrap_or_else(|| alias.node.name.clone()),
),
used: None,
location: stmt.location,
location: Range::from_located(stmt),
},
)
}
}
}
StmtKind::ImportFrom { names, module, .. } => {
StmtKind::ImportFrom {
names,
module,
level,
} => {
if self
.settings
.select
@@ -401,7 +468,7 @@ where
{
self.checks.push(Check::new(
CheckKind::ModuleImportNotAtTopOfFile,
stmt.location,
self.locate_check(Range::from_located(stmt)),
));
}
@@ -422,9 +489,9 @@ where
.last()
.expect("No current scope found."))]
.id,
stmt.location,
Range::from_located(stmt),
)),
location: stmt.location,
location: Range::from_located(stmt),
},
);
@@ -437,54 +504,68 @@ where
{
self.checks.push(Check::new(
CheckKind::FutureFeatureNotDefined(alias.node.name.to_string()),
stmt.location,
self.locate_check(Range::from_located(stmt)),
));
}
if self.settings.select.contains(&CheckCode::F404) && !self.futures_allowed
{
self.checks
.push(Check::new(CheckKind::LateFutureImport, stmt.location));
self.checks.push(Check::new(
CheckKind::LateFutureImport,
self.locate_check(Range::from_located(stmt)),
));
}
} else if alias.node.name == "*" {
let module_name = format!(
"{}{}",
".".repeat(level.unwrap_or_default()),
module.clone().unwrap_or_else(|| "module".to_string()),
);
self.add_binding(
name,
module_name.to_string(),
Binding {
kind: BindingKind::StarImportation,
used: None,
location: stmt.location,
location: Range::from_located(stmt),
},
);
if self.settings.select.contains(&CheckCode::F403) {
self.checks.push(Check::new(
CheckKind::ImportStarUsage(
module.clone().unwrap_or_else(|| "module".to_string()),
),
stmt.location,
));
}
if self.settings.select.contains(&CheckCode::F406) {
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if !matches!(scope.kind, ScopeKind::Module) {
self.checks.push(Check::new(
CheckKind::ImportStarNotPermitted(
module.clone().unwrap_or_else(|| "module".to_string()),
),
stmt.location,
CheckKind::ImportStarNotPermitted(module_name.to_string()),
self.locate_check(Range::from_located(stmt)),
));
}
}
if self.settings.select.contains(&CheckCode::F403) {
self.checks.push(Check::new(
CheckKind::ImportStarUsed(module_name.to_string()),
self.locate_check(Range::from_located(stmt)),
));
}
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
scope.import_starred = true;
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
}
let binding = Binding {
kind: BindingKind::Importation(match module {
None => name.clone(),
Some(parent) => format!("{}.{}", parent, name),
}),
used: None,
location: stmt.location,
location: Range::from_located(stmt),
};
self.add_binding(name, binding)
}
@@ -504,14 +585,19 @@ where
}
StmtKind::If { test, .. } => {
if self.settings.select.contains(&CheckCode::F634) {
if let Some(check) = checks::check_if_tuple(test, stmt.location) {
if let Some(check) =
checks::check_if_tuple(test, self.locate_check(Range::from_located(stmt)))
{
self.checks.push(check);
}
}
}
StmtKind::Assert { test, .. } => {
if self.settings.select.contains(CheckKind::AssertTuple.code()) {
if let Some(check) = checks::check_assert_tuple(test, stmt.location) {
if let Some(check) = checks::check_assert_tuple(
test,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
}
@@ -525,7 +611,10 @@ where
}
StmtKind::Assign { value, .. } => {
if self.settings.select.contains(&CheckCode::E731) {
if let Some(check) = checks::check_do_not_assign_lambda(value, stmt.location) {
if let Some(check) = checks::check_do_not_assign_lambda(
value,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
}
@@ -533,9 +622,10 @@ where
StmtKind::AnnAssign { value, .. } => {
if self.settings.select.contains(&CheckCode::E731) {
if let Some(value) = value {
if let Some(check) =
checks::check_do_not_assign_lambda(value, stmt.location)
{
if let Some(check) = checks::check_do_not_assign_lambda(
value,
self.locate_check(Range::from_located(stmt)),
) {
self.checks.push(check);
}
}
@@ -570,7 +660,7 @@ where
Binding {
kind: BindingKind::ClassDefinition,
used: None,
location: stmt.location,
location: Range::from_located(stmt),
},
);
};
@@ -590,7 +680,6 @@ where
let prev_in_literal = self.in_literal;
let prev_in_annotation = self.in_annotation;
// Important:
if self.in_annotation && self.annotations_future_enabled {
self.deferred_annotations.push((
expr,
@@ -616,9 +705,9 @@ where
self.settings.select.contains(&CheckCode::F622);
if let Some(check) = checks::check_starred_expressions(
elts,
expr.location,
check_too_many_expressions,
check_two_starred_expressions,
self.locate_check(Range::from_located(expr)),
) {
self.checks.push(check);
}
@@ -628,24 +717,52 @@ where
ExprContext::Load => self.handle_node_load(expr),
ExprContext::Store => {
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) =
checks::check_ambiguous_variable_name(id, expr.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
id,
self.locate_check(Range::from_located(expr)),
) {
self.checks.push(check);
}
}
self.check_builtin_shadowing(id, Range::from_located(expr), true);
let parent =
self.parents[*(self.parent_stack.last().expect("No parent found."))];
self.handle_node_store(expr, parent);
}
ExprContext::Del => self.handle_node_delete(expr),
},
ExprKind::Call { func, .. } => {
ExprKind::Call { func, args, .. } => {
if self.settings.select.contains(&CheckCode::R002) {
if let Some(check) = checks::check_assert_equals(func, self.autofix) {
self.checks.push(check)
}
}
// flake8-super
if self.settings.select.contains(&CheckCode::SPR001) {
if let Some(check) =
checks::check_super_args(expr, func, args, &mut self.locator, self.autofix)
{
self.checks.push(check)
}
}
if let ExprKind::Name { id, ctx } = &func.node {
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: false })
) {
scope.kind = ScopeKind::Function(FunctionScope { uses_locals: true });
}
}
}
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.select.contains(&CheckCode::F601);
@@ -655,6 +772,7 @@ where
keys,
check_repeated_literals,
check_repeated_variables,
self,
));
}
}
@@ -667,12 +785,14 @@ where
.contains(CheckKind::YieldOutsideFunction.code())
&& matches!(scope.kind, ScopeKind::Class | ScopeKind::Module)
{
self.checks
.push(Check::new(CheckKind::YieldOutsideFunction, expr.location));
self.checks.push(Check::new(
CheckKind::YieldOutsideFunction,
self.locate_check(Range::from_located(expr)),
));
}
}
ExprKind::JoinedStr { values } => {
if !self.in_f_string
if self.in_f_string.is_none()
&& self
.settings
.select
@@ -683,10 +803,10 @@ where
{
self.checks.push(Check::new(
CheckKind::FStringMissingPlaceholders,
expr.location,
self.locate_check(Range::from_located(expr)),
));
}
self.in_f_string = true;
self.in_f_string = Some(Range::from_located(expr));
}
ExprKind::BinOp {
left,
@@ -703,8 +823,10 @@ where
..
}) = scope.values.get("print")
{
self.checks
.push(Check::new(CheckKind::InvalidPrintSyntax, left.location));
self.checks.push(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}
}
}
@@ -719,6 +841,7 @@ where
operand,
check_not_in,
check_not_is,
self,
));
}
}
@@ -736,6 +859,7 @@ where
comparators,
check_none_comparisons,
check_true_false_comparisons,
self,
));
}
@@ -744,7 +868,7 @@ where
left,
ops,
comparators,
expr.location,
self.locate_check(Range::from_located(expr)),
));
}
@@ -752,7 +876,7 @@ where
self.checks.extend(checks::check_type_comparison(
ops,
comparators,
expr.location,
self.locate_check(Range::from_located(expr)),
));
}
}
@@ -761,7 +885,7 @@ where
..
} if self.in_annotation && !self.in_literal => {
self.deferred_string_annotations
.push((expr.location, value));
.push((Range::from_located(expr), value));
}
ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
@@ -911,18 +1035,26 @@ where
if self.settings.select.contains(&CheckCode::E722) && type_.is_none() {
self.checks.push(Check::new(
CheckKind::DoNotUseBareExcept,
excepthandler.location,
Range::from_located(excepthandler),
));
}
match name {
Some(name) => {
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) =
checks::check_ambiguous_variable_name(name, excepthandler.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
name,
self.locate_check(Range::from_located(excepthandler)),
) {
self.checks.push(check);
}
}
self.check_builtin_shadowing(
name,
Range::from_located(excepthandler),
false,
);
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if scope.values.contains_key(name) {
@@ -931,6 +1063,7 @@ where
self.handle_node_store(
&Expr::new(
excepthandler.location,
excepthandler.end_location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
@@ -949,6 +1082,7 @@ where
self.handle_node_store(
&Expr::new(
excepthandler.location,
excepthandler.end_location,
ExprKind::Name {
id: name.to_string(),
ctx: ExprContext::Store,
@@ -968,7 +1102,7 @@ where
{
self.checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
excepthandler.location,
Range::from_located(excepthandler),
));
}
}
@@ -1014,16 +1148,26 @@ where
Binding {
kind: BindingKind::Argument,
used: None,
location: arg.location,
location: Range::from_located(arg),
},
);
if self.settings.select.contains(&CheckCode::E741) {
if let Some(check) = checks::check_ambiguous_variable_name(&arg.node.arg, arg.location)
{
if let Some(check) = checks::check_ambiguous_variable_name(
&arg.node.arg,
self.locate_check(Range::from_located(arg)),
) {
self.checks.push(check);
}
}
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
}
}
impl CheckLocator for Checker<'_> {
fn locate_check(&self, default: Range) -> Range {
self.in_f_string.unwrap_or(default)
}
}
@@ -1083,12 +1227,27 @@ impl<'a> Checker<'a> {
// TODO(charlie): Don't treat annotations as assignments if there is an existing value.
let binding = match scope.values.get(&name) {
None => binding,
Some(existing) => Binding {
kind: binding.kind,
location: binding.location,
used: existing.used,
},
Some(existing) => {
if self.settings.select.contains(&CheckCode::F402)
&& matches!(existing.kind, BindingKind::Importation(_))
&& matches!(binding.kind, BindingKind::LoopVar)
{
self.checks.push(Check::new(
CheckKind::ImportShadowedByLoopVar(
name.clone(),
existing.location.location.row(),
),
binding.location,
));
}
Binding {
kind: binding.kind,
location: binding.location,
used: existing.used,
}
}
};
scope.values.insert(name, binding);
}
@@ -1099,6 +1258,7 @@ impl<'a> Checker<'a> {
let mut first_iter = true;
let mut in_generator = false;
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class) {
@@ -1109,12 +1269,34 @@ impl<'a> Checker<'a> {
}
}
if let Some(binding) = scope.values.get_mut(id) {
binding.used = Some((scope_id, expr.location));
binding.used = Some((scope_id, Range::from_located(expr)));
return;
}
first_iter = false;
in_generator = matches!(scope.kind, ScopeKind::Generator);
import_starred = import_starred || scope.import_starred;
}
if import_starred {
if self.settings.select.contains(&CheckCode::F405) {
let mut from_list = vec![];
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.as_str());
}
}
}
from_list.sort();
self.checks.push(Check::new(
CheckKind::ImportStarUsage(id.clone(), from_list.join(", ")),
self.locate_check(Range::from_located(expr)),
));
}
return;
}
if self.settings.select.contains(&CheckCode::F821) {
@@ -1124,7 +1306,7 @@ impl<'a> Checker<'a> {
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(Range::from_located(expr)),
))
}
}
@@ -1136,17 +1318,17 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& matches!(current.kind, ScopeKind::Function(_))
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function | ScopeKind::Module) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(id) {
if let Some((scope_id, location)) = binding.used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
location,
self.locate_check(location),
));
}
}
@@ -1161,7 +1343,7 @@ impl<'a> Checker<'a> {
Binding {
kind: BindingKind::Annotation,
used: None,
location: expr.location,
location: Range::from_located(expr),
},
);
return;
@@ -1171,14 +1353,25 @@ impl<'a> Checker<'a> {
if matches!(
parent.node,
StmtKind::For { .. } | StmtKind::AsyncFor { .. }
) || operations::is_unpacking_assignment(parent)
{
) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::LoopVar,
used: None,
location: Range::from_located(expr),
},
);
return;
}
if operations::is_unpacking_assignment(parent) {
self.add_binding(
id.to_string(),
Binding {
kind: BindingKind::Binding,
used: None,
location: expr.location,
location: Range::from_located(expr),
},
);
return;
@@ -1198,7 +1391,7 @@ impl<'a> Checker<'a> {
Binding {
kind: BindingKind::Export(extract_all_names(parent, current)),
used: None,
location: expr.location,
location: Range::from_located(expr),
},
);
return;
@@ -1209,7 +1402,7 @@ impl<'a> Checker<'a> {
Binding {
kind: BindingKind::Assignment,
used: None,
location: expr.location,
location: Range::from_located(expr),
},
);
}
@@ -1228,7 +1421,7 @@ impl<'a> Checker<'a> {
{
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
self.locate_check(Range::from_located(expr)),
))
}
}
@@ -1253,7 +1446,7 @@ impl<'a> Checker<'a> {
} else if self.settings.select.contains(&CheckCode::F722) {
self.checks.push(Check::new(
CheckKind::ForwardAnnotationSyntaxError(expression.to_string()),
location,
self.locate_check(location),
));
}
}
@@ -1266,7 +1459,7 @@ impl<'a> Checker<'a> {
while let Some((stmt, scopes, parents)) = self.deferred_functions.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
match &stmt.node {
StmtKind::FunctionDef { body, args, .. }
@@ -1290,7 +1483,7 @@ impl<'a> Checker<'a> {
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
if let ExprKind::Lambda { args, body } = &expr.node {
self.visit_arguments(args);
@@ -1305,17 +1498,21 @@ impl<'a> Checker<'a> {
}
fn check_deferred_assignments(&mut self) {
while let Some(index) = self.deferred_assignments.pop() {
if self.settings.select.contains(&CheckCode::F841) {
self.checks
.extend(checks::check_unused_variables(&self.scopes[index]));
if self.settings.select.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
self.checks.extend(checks::check_unused_variables(
&self.scopes[index],
self,
&self.settings.dummy_variable_rgx,
));
}
}
}
fn check_dead_scopes(&mut self) {
if !self.settings.select.contains(&CheckCode::F822)
&& !self.settings.select.contains(&CheckCode::F401)
if !self.settings.select.contains(&CheckCode::F401)
&& !self.settings.select.contains(&CheckCode::F405)
&& !self.settings.select.contains(&CheckCode::F822)
{
return;
}
@@ -1330,15 +1527,39 @@ impl<'a> Checker<'a> {
});
if self.settings.select.contains(&CheckCode::F822)
&& !scope.import_starred
&& !self.path.ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check::new(
CheckKind::UndefinedExport(name.to_string()),
binding.location,
self.locate_check(all_binding.location),
));
}
}
}
}
}
if self.settings.select.contains(&CheckCode::F405) && scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = all_names {
let mut from_list = vec![];
for (name, binding) in scope.values.iter() {
if matches!(binding.kind, BindingKind::StarImportation) {
from_list.push(name.as_str());
}
}
from_list.sort();
for name in names {
if !scope.values.contains_key(name) {
self.checks.push(Check::new(
CheckKind::ImportStarUsage(name.clone(), from_list.join(", ")),
self.locate_check(all_binding.location),
));
}
}
@@ -1359,7 +1580,7 @@ impl<'a> Checker<'a> {
| BindingKind::SubmoduleImportation(full_name) => {
self.checks.push(Check::new(
CheckKind::UnusedImport(full_name.to_string()),
binding.location,
self.locate_check(binding.location),
));
}
_ => {}
@@ -1369,6 +1590,43 @@ impl<'a> Checker<'a> {
}
}
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
// flake8-builtins
if is_attribute && matches!(scope.kind, ScopeKind::Class) {
if self.settings.select.contains(&CheckCode::A003) {
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Attribute,
) {
self.checks.push(check);
}
}
} else if self.settings.select.contains(&CheckCode::A001) {
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Variable,
) {
self.checks.push(check);
}
}
}
fn check_builtin_arg_shadowing(&mut self, name: &str, location: Range) {
if self.settings.select.contains(&CheckCode::A002) {
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Argument,
) {
self.checks.push(check);
}
}
}
}
pub fn check_ast(

View File

@@ -1,6 +1,12 @@
use std::collections::BTreeMap;
use crate::ast::types::Range;
use rustpython_parser::ast::Location;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
/// Whether the given line is too long and should be reported.
@@ -19,37 +25,205 @@ fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
}
}
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
pub fn check_lines(
checks: &mut Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
settings: &Settings,
autofix: &fixer::Mode,
) {
let enforce_line_too_long = settings.select.contains(&CheckCode::E501);
let enforce_noqa = settings.select.contains(&CheckCode::M001);
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
let mut line_checks = vec![];
let mut ignored = vec![];
for (row, line) in contents.lines().enumerate() {
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
}
// Remove any ignored checks.
// TODO(charlie): Only validate checks for the current line.
for (index, check) in checks.iter().enumerate() {
if check.location.row() == row + 1 && check.is_inline_ignored(line) {
ignored.push(index);
if check.location.row() == lineno + 1 {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
ignored.push(index);
}
}
(Directive::None, _) => {}
}
}
}
// Enforce line length.
if enforce_line_too_long {
let line_length = line.len();
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Location::new(row + 1, settings.line_length + 1),
Range {
location: Location::new(lineno + 1, 1),
end_location: Location::new(lineno + 1, line_length + 1),
},
);
if !check.is_inline_ignored(line) {
line_checks.push(check);
match noqa {
(Directive::All(_, _), matches) => {
matches.push(check.kind.code().as_str());
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_str()) {
matches.push(check.kind.code().as_str());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
}
}
}
// Enforce that the noqa directive was actually used.
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(start, end) => {
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
},
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "".to_string(),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
}
line_checks.push(check);
}
}
Directive::Codes(start, end, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
} else {
valid_codes.push(code);
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),
},
);
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if valid_codes.is_empty() {
check.amend(Fix {
content: "".to_string(),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
} else {
check.amend(Fix {
content: format!(" # noqa: {}", valid_codes.join(", ")),
location: Location::new(row + 1, start + 1),
end_location: Location::new(
row + 1,
lines[row].chars().count() + 1,
),
applied: false,
});
}
}
line_checks.push(check);
}
}
Directive::None => {}
}
}
}
ignored.sort();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
checks.extend(line_checks);
}
#[cfg(test)]
mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::settings;
use super::check_lines;
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let noqa_line_for: Vec<usize> = vec![1];
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
check_lines(
&mut checks,
line,
&noqa_line_for,
&settings::Settings {
line_length,
..settings::Settings::for_rule(CheckCode::E501)
},
&fixer::Mode::Generate,
);
return checks;
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

View File

@@ -1,13 +1,124 @@
use std::str::FromStr;
use crate::ast::types::Range;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub const DEFAULT_CHECK_CODES: [CheckCode; 46] = [
// pycodestyle
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
// pyflakes
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
// flake8-builtins
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
// flake8-super
CheckCode::SPR001,
];
pub const ALL_CHECK_CODES: [CheckCode; 49] = [
// pycodestyle
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
// pyflakes
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
// flake8-builtins
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
// flake8-super
CheckCode::SPR001,
// Meta
CheckCode::M001,
// Refactor
CheckCode::R001,
CheckCode::R002,
];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CheckCode {
// pycodestyle
E402,
E501,
E711,
@@ -22,9 +133,12 @@ pub enum CheckCode {
E743,
E902,
E999,
// pyflakes
F401,
F402,
F403,
F404,
F405,
F406,
F407,
F541,
@@ -48,8 +162,17 @@ pub enum CheckCode {
F831,
F841,
F901,
// flake8-builtins
A001,
A002,
A003,
// flake8-super
SPR001,
// Refactor
R001,
R002,
// Meta
M001,
}
impl FromStr for CheckCode {
@@ -57,6 +180,7 @@ impl FromStr for CheckCode {
fn from_str(s: &str) -> Result<Self> {
match s {
// pycodestyle
"E402" => Ok(CheckCode::E402),
"E501" => Ok(CheckCode::E501),
"E711" => Ok(CheckCode::E711),
@@ -71,9 +195,12 @@ impl FromStr for CheckCode {
"E743" => Ok(CheckCode::E743),
"E902" => Ok(CheckCode::E902),
"E999" => Ok(CheckCode::E999),
// pyflakes
"F401" => Ok(CheckCode::F401),
"F402" => Ok(CheckCode::F402),
"F403" => Ok(CheckCode::F403),
"F404" => Ok(CheckCode::F404),
"F405" => Ok(CheckCode::F405),
"F406" => Ok(CheckCode::F406),
"F407" => Ok(CheckCode::F407),
"F541" => Ok(CheckCode::F541),
@@ -97,8 +224,17 @@ impl FromStr for CheckCode {
"F831" => Ok(CheckCode::F831),
"F841" => Ok(CheckCode::F841),
"F901" => Ok(CheckCode::F901),
// flake8-builtins
"A001" => Ok(CheckCode::A001),
"A002" => Ok(CheckCode::A002),
"A003" => Ok(CheckCode::A003),
// flake8-super
"SPR001" => Ok(CheckCode::SPR001),
// Refactor
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
// Meta
"M001" => Ok(CheckCode::M001),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -107,6 +243,7 @@ impl FromStr for CheckCode {
impl CheckCode {
pub fn as_str(&self) -> &str {
match self {
// pycodestyle
CheckCode::E402 => "E402",
CheckCode::E501 => "E501",
CheckCode::E711 => "E711",
@@ -121,9 +258,12 @@ impl CheckCode {
CheckCode::E743 => "E743",
CheckCode::E902 => "E902",
CheckCode::E999 => "E999",
// pyflakes
CheckCode::F401 => "F401",
CheckCode::F402 => "F402",
CheckCode::F403 => "F403",
CheckCode::F404 => "F404",
CheckCode::F405 => "F405",
CheckCode::F406 => "F406",
CheckCode::F407 => "F407",
CheckCode::F541 => "F541",
@@ -147,19 +287,89 @@ impl CheckCode {
CheckCode::F831 => "F831",
CheckCode::F841 => "F841",
CheckCode::F901 => "F901",
// flake8-builtins
CheckCode::A001 => "A001",
CheckCode::A002 => "A002",
CheckCode::A003 => "A003",
// flake8-super
CheckCode::SPR001 => "SPR001",
// Refactor
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
// Meta
CheckCode::M001 => "M001",
}
}
/// The source for the check (either the AST, the filesystem, or the physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 => &LintSource::Lines,
CheckCode::E902 | CheckCode::E999 => &LintSource::FileSystem,
CheckCode::E501 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E902 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
}
/// A placeholder representation of the CheckKind for the check.
pub fn kind(&self) -> CheckKind {
match self {
// pycodestyle
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckCode::E713 => CheckKind::NotInTest,
CheckCode::E714 => CheckKind::NotIsTest,
CheckCode::E721 => CheckKind::TypeComparison,
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
CheckCode::E731 => CheckKind::DoNotAssignLambda,
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
CheckCode::E902 => CheckKind::IOError("...".to_string()),
CheckCode::E999 => CheckKind::SyntaxError("...".to_string()),
// pyflakes
CheckCode::F401 => CheckKind::UnusedImport("...".to_string()),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
CheckCode::F405 => CheckKind::ImportStarUsage("...".to_string(), "...".to_string()),
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckCode::F621 => CheckKind::TooManyExpressionsInStarredAssignment,
CheckCode::F622 => CheckKind::TwoStarredExpressions,
CheckCode::F631 => CheckKind::AssertTuple,
CheckCode::F632 => CheckKind::IsLiteral,
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F701 => CheckKind::BreakOutsideLoop,
CheckCode::F702 => CheckKind::ContinueOutsideLoop,
CheckCode::F704 => CheckKind::YieldOutsideFunction,
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
CheckCode::F707 => CheckKind::DefaultExceptNotLast,
CheckCode::F722 => CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
CheckCode::F822 => CheckKind::UndefinedExport("...".to_string()),
CheckCode::F823 => CheckKind::UndefinedLocal("...".to_string()),
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
CheckCode::F901 => CheckKind::RaiseNotImplemented,
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
// flake8-super
CheckCode::SPR001 => CheckKind::SuperCallWithParameters,
// Refactor
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
}
}
#[allow(clippy::upper_case_acronyms)]
@@ -177,6 +387,7 @@ pub enum RejectedCmpop {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind {
UnusedNOQA(Option<String>),
AmbiguousClassName(String),
AmbiguousFunctionName(String),
AmbiguousVariableName(String),
@@ -192,8 +403,10 @@ pub enum CheckKind {
FutureFeatureNotDefined(String),
IOError(String),
IfTuple,
ImportShadowedByLoopVar(String, usize),
ImportStarNotPermitted(String),
ImportStarUsage(String),
ImportStarUsage(String, String),
ImportStarUsed(String),
InvalidPrintSyntax,
IsLiteral,
LateFutureImport,
@@ -219,6 +432,12 @@ pub enum CheckKind {
UnusedVariable(String),
UselessObjectInheritance(String),
YieldOutsideFunction,
// flake8-builtin
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-super
SuperCallWithParameters,
}
impl CheckKind {
@@ -240,8 +459,10 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(_) => "FutureFeatureNotDefined",
CheckKind::IOError(_) => "IOError",
CheckKind::IfTuple => "IfTuple",
CheckKind::ImportShadowedByLoopVar(_, _) => "ImportShadowedByLoopVar",
CheckKind::ImportStarNotPermitted(_) => "ImportStarNotPermitted",
CheckKind::ImportStarUsage(_) => "ImportStarUsage",
CheckKind::ImportStarUsage(_, _) => "ImportStarUsage",
CheckKind::ImportStarUsed(_) => "ImportStarUsed",
CheckKind::InvalidPrintSyntax => "InvalidPrintSyntax",
CheckKind::IsLiteral => "IsLiteral",
CheckKind::LateFutureImport => "LateFutureImport",
@@ -269,6 +490,13 @@ impl CheckKind {
CheckKind::UnusedVariable(_) => "UnusedVariable",
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
CheckKind::UnusedNOQA(_) => "UnusedNOQA",
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => "BuiltinVariableShadowing",
CheckKind::BuiltinArgumentShadowing(_) => "BuiltinArgumentShadowing",
CheckKind::BuiltinAttributeShadowing(_) => "BuiltinAttributeShadowing",
// flake8-super
CheckKind::SuperCallWithParameters => "SuperCallWithParameters",
}
}
@@ -290,8 +518,10 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(_) => &CheckCode::F407,
CheckKind::IOError(_) => &CheckCode::E902,
CheckKind::IfTuple => &CheckCode::F634,
CheckKind::ImportShadowedByLoopVar(_, _) => &CheckCode::F402,
CheckKind::ImportStarNotPermitted(_) => &CheckCode::F406,
CheckKind::ImportStarUsage(_) => &CheckCode::F403,
CheckKind::ImportStarUsage(_, _) => &CheckCode::F405,
CheckKind::ImportStarUsed(_) => &CheckCode::F403,
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
@@ -314,9 +544,16 @@ impl CheckKind {
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-super
CheckKind::SuperCallWithParameters => &CheckCode::SPR001,
}
}
@@ -356,17 +593,21 @@ impl CheckKind {
CheckKind::FutureFeatureNotDefined(name) => {
format!("future feature '{name}' is not defined")
}
CheckKind::IOError(name) => {
format!("No such file or directory: `{name}`")
}
CheckKind::IOError(message) => message.clone(),
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
CheckKind::InvalidPrintSyntax => "use of >> is invalid with print function".to_string(),
CheckKind::ImportShadowedByLoopVar(name, line) => {
format!("import '{name}' from line {line} shadowed by loop variable")
}
CheckKind::ImportStarNotPermitted(name) => {
format!("`from {name} import *` only allowed at module level")
}
CheckKind::ImportStarUsage(name) => {
CheckKind::ImportStarUsed(name) => {
format!("`from {name} import *` used; unable to detect undefined names")
}
CheckKind::ImportStarUsage(name, sources) => {
format!("'{name}' may be undefined, or defined from star imports: {sources}")
}
CheckKind::IsLiteral => "use ==/!= to compare constant literals".to_string(),
CheckKind::LateFutureImport => {
"from __future__ imports must occur at the beginning of the file".to_string()
@@ -443,6 +684,24 @@ impl CheckKind {
CheckKind::YieldOutsideFunction => {
"a `yield` or `yield from` statement outside of a function/method".to_string()
}
CheckKind::UnusedNOQA(code) => match code {
None => "Unused `noqa` directive".to_string(),
Some(code) => format!("Unused `noqa` directive for: {code}"),
},
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
}
CheckKind::BuiltinArgumentShadowing(name) => {
format!("Argument `{name}` is shadowing a python builtin")
}
CheckKind::BuiltinAttributeShadowing(name) => {
format!("class attribute `{name}` is shadowing a python builtin")
}
// flake8-super
CheckKind::SuperCallWithParameters => {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
}
}
@@ -450,7 +709,10 @@ impl CheckKind {
pub fn fixable(&self) -> bool {
matches!(
self,
CheckKind::NoAssertEquals | CheckKind::UselessObjectInheritance(_)
CheckKind::NoAssertEquals
| CheckKind::UselessObjectInheritance(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::SuperCallWithParameters
)
}
}
@@ -458,8 +720,8 @@ impl CheckKind {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fix {
pub content: String,
pub start: Location,
pub end: Location,
pub location: Location,
pub end_location: Location,
pub applied: bool,
}
@@ -467,19 +729,16 @@ pub struct Fix {
pub struct Check {
pub kind: CheckKind,
pub location: Location,
pub end_location: Location,
pub fix: Option<Fix>,
}
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?").expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
impl Check {
pub fn new(kind: CheckKind, location: Location) -> Self {
pub fn new(kind: CheckKind, span: Range) -> Self {
Self {
kind,
location,
location: span.location,
end_location: span.end_location,
fix: None,
}
}
@@ -487,25 +746,4 @@ impl Check {
pub fn amend(&mut self, fix: Fix) {
self.fix = Some(fix);
}
pub fn is_inline_ignored(&self, line: &str) -> bool {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("codes") {
Some(codes) => {
for code in SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
{
if code == self.kind.code().as_str() {
return true;
}
}
false
}
None => true,
},
None => false,
}
}
}

283
src/fs.rs
View File

@@ -1,35 +1,57 @@
use std::env;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use anyhow::{anyhow, Result};
use log::debug;
use path_absolutize::path_dedot;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool {
// Check the basename.
if let Some(file_name) = path.file_name() {
if let Some(file_name) = file_name.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
use crate::checks::CheckCode;
use crate::settings::{FilePattern, PerFileIgnore};
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
let file_path = path
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
let file_basename = path
.file_name()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
Ok((file_path, file_basename))
}
fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool
where
T: Iterator<Item = &'a FilePattern>,
{
for pattern in exclude {
match pattern {
FilePattern::Simple(basename) => {
if *basename == file_basename {
return true;
}
}
}
}
// Check the complete path.
if let Some(file_name) = path.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
return true;
FilePattern::Complex(absolute, basename) => {
if absolute.matches(file_path) {
return true;
}
if basename
.as_ref()
.map(|pattern| pattern.matches(file_basename))
.unwrap_or_default()
{
return true;
}
}
}
};
}
false
}
@@ -39,50 +61,107 @@ fn is_included(path: &Path) -> bool {
}
pub fn iter_python_files<'a>(
path: &'a PathBuf,
exclude: &'a [Pattern],
extend_exclude: &'a [Pattern],
) -> impl Iterator<Item = DirEntry> + 'a {
path: &'a Path,
exclude: &'a [FilePattern],
extend_exclude: &'a [FilePattern],
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
let exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
let extend_exclude_simple = extend_exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(|entry| {
if exclude.is_empty() && extend_exclude.is_empty() {
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path();
if is_excluded(path, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if is_excluded(path, extend_exclude) {
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
let file_type = entry.file_type();
if has_exclude
&& (!exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, exclude.iter())
{
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& (!extend_exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, extend_exclude.iter())
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(_) => {
debug!("Ignored path due to error in parsing: {:?}", path);
true
}
}
})
.filter_map(|entry| entry.ok())
.filter(|entry| {
let path = entry.path();
is_included(path)
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 || is_included(entry.path()))
&& !entry.file_type().is_dir()
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
})
})
}
pub fn normalize_path(path: &PathBuf) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.clone();
}
if let Ok(path) = path.absolutize() {
if let Ok(root) = env::current_dir() {
if let Ok(path) = path.strip_prefix(root) {
return Path::new(".").join(path);
}
}
}
path.clone()
/// Create tree set with codes matching the pattern/code pairs.
pub fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [PerFileIgnore],
) -> Result<BTreeSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
.filter(|pattern_code_pair| {
is_excluded(
file_path,
file_basename,
[&pattern_code_pair.pattern].into_iter(),
)
})
.map(|pattern_code_pair| &pattern_code_pair.code)
.collect())
}
/// Convert any path to an absolute path (based on the current working directory).
pub fn normalize_path(path: &Path) -> PathBuf {
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert any path to an absolute path (based on the specified project root).
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) {
return path.to_string_lossy();
}
path.to_string_lossy()
}
/// Read a file's contents from disk.
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
@@ -95,53 +174,95 @@ pub fn read_file(path: &Path) -> Result<String> {
mod tests {
use std::path::Path;
use glob::Pattern;
use anyhow::Result;
use path_absolutize::Absolutize;
use crate::fs::{is_excluded, is_included};
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.pyi");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.js");
assert!(!is_included(path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_included(&path));
let path = Path::new("foo/bar/baz");
assert!(!is_included(path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
assert!(!is_included(&path));
}
#[test]
fn exclusions() {
let path = Path::new("foo");
let exclude = vec![Pattern::new("foo").unwrap()];
assert!(is_excluded(path, &exclude));
fn exclusions() -> Result<()> {
let project_root = Path::new("/tmp/");
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("foo/bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/*.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz").unwrap()];
assert!(!is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, exclude.iter()));
Ok(())
}
}

View File

@@ -1,4 +1,13 @@
extern crate core;
use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer::Mode;
use crate::linter::{check_path, tokenize};
use crate::message::Message;
use crate::settings::Settings;
mod ast;
mod autofix;
@@ -10,7 +19,55 @@ pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod noqa;
pub mod printer;
mod pyproject;
pub mod pyproject;
mod python;
pub mod settings;
/// Run ruff over Python source code directly.
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_pyproject(pyproject, project_root)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(
path,
contents,
tokens,
&noqa_line_for,
&settings,
&Mode::None,
)?;
// Convert to messages.
let messages: Vec<Message> = checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
end_location: check.end_location,
filename: path.to_string_lossy().to_string(),
})
.collect();
Ok(messages)
}

View File

@@ -2,20 +2,38 @@ use std::path::Path;
use anyhow::Result;
use log::debug;
use rustpython_parser::parser;
use rustpython_parser::lexer::LexResult;
use rustpython_parser::{lexer, parser};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::autofix::fixer::fix_file;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::checks::{Check, LintSource};
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::settings::Settings;
use crate::{cache, fs};
use crate::{cache, fs, noqa};
fn check_path(
/// Collect tokens up to and including the first error.
pub(crate) fn tokenize(contents: &str) -> Vec<LexResult> {
let mut tokens: Vec<LexResult> = vec![];
for tok in lexer::make_tokenizer(contents) {
let is_err = tok.is_err();
tokens.push(tok);
if is_err {
break;
}
}
tokens
}
pub(crate) fn check_path(
path: &Path,
contents: &str,
tokens: Vec<LexResult>,
noqa_line_for: &[usize],
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
@@ -28,12 +46,37 @@ fn check_path(
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
let python_ast = parser::parse_program(contents, "<filename>")?;
checks.extend(check_ast(&python_ast, contents, settings, autofix, path));
match parser::parse_program_tokens(tokens, "<filename>") {
Ok(python_ast) => {
checks.extend(check_ast(&python_ast, contents, settings, autofix, path))
}
Err(parse_error) => {
if settings.select.contains(&CheckCode::E999) {
checks.push(Check::new(
CheckKind::SyntaxError(parse_error.error.to_string()),
Range {
location: parse_error.location,
end_location: parse_error.location,
},
))
}
}
}
}
// Run the lines-based checks.
check_lines(&mut checks, contents, settings);
check_lines(&mut checks, contents, noqa_line_for, settings, autofix);
// Create path ignores.
if !checks.is_empty() && !settings.per_file_ignores.is_empty() {
let ignores = fs::ignores_from_path(path, &settings.per_file_ignores)?;
if !ignores.is_empty() {
return Ok(checks
.into_iter()
.filter(|check| !ignores.contains(check.kind.code()))
.collect());
}
}
Ok(checks)
}
@@ -55,8 +98,14 @@ pub fn lint_path(
// Read the file from disk.
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let mut checks = check_path(path, &contents, settings, autofix)?;
let mut checks = check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
@@ -70,6 +119,7 @@ pub fn lint_path(
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
end_location: check.end_location,
filename: path.to_string_lossy().to_string(),
})
.collect();
@@ -78,18 +128,43 @@ pub fn lint_path(
Ok(messages)
}
pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Read the file from disk.
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(
path,
&contents,
tokens,
&noqa_line_for,
settings,
&fixer::Mode::None,
)?;
add_noqa(&checks, &contents, &noqa_line_for, path)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
use regex::Regex;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::fs;
use crate::linter;
use crate::linter::tokenize;
use crate::settings;
use crate::{fs, noqa};
fn check_path(
path: &Path,
@@ -97,19 +172,16 @@ mod tests {
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
linter::check_path(path, &contents, settings, autofix)
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
}
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E402]),
},
&settings::Settings::for_rule(CheckCode::E402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -121,12 +193,7 @@ mod tests {
fn e501() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E501.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E501]),
},
&settings::Settings::for_rule(CheckCode::E501),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -138,12 +205,7 @@ mod tests {
fn e711() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E711.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E711]),
},
&settings::Settings::for_rule(CheckCode::E711),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -155,12 +217,7 @@ mod tests {
fn e712() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E712.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E712]),
},
&settings::Settings::for_rule(CheckCode::E712),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -172,12 +229,7 @@ mod tests {
fn e713() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E713.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E713]),
},
&settings::Settings::for_rule(CheckCode::E713),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -189,12 +241,7 @@ mod tests {
fn e721() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E721.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E721]),
},
&settings::Settings::for_rule(CheckCode::E721),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -206,12 +253,7 @@ mod tests {
fn e722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E722.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E722]),
},
&settings::Settings::for_rule(CheckCode::E722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -223,12 +265,7 @@ mod tests {
fn e714() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E714.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E714]),
},
&settings::Settings::for_rule(CheckCode::E714),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -240,12 +277,7 @@ mod tests {
fn e731() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E731.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E731]),
},
&settings::Settings::for_rule(CheckCode::E731),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -257,12 +289,7 @@ mod tests {
fn e741() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E741.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E741]),
},
&settings::Settings::for_rule(CheckCode::E741),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -274,12 +301,7 @@ mod tests {
fn e742() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E742.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E742]),
},
&settings::Settings::for_rule(CheckCode::E742),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -291,12 +313,7 @@ mod tests {
fn e743() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E743.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::E743]),
},
&settings::Settings::for_rule(CheckCode::E743),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -308,12 +325,19 @@ mod tests {
fn f401() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F401.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F401]),
},
&settings::Settings::for_rule(CheckCode::F401),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F402.py"),
&settings::Settings::for_rule(CheckCode::F402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -325,12 +349,7 @@ mod tests {
fn f403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F403.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F403]),
},
&settings::Settings::for_rule(CheckCode::F403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -342,12 +361,19 @@ mod tests {
fn f404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F404.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F404]),
},
&settings::Settings::for_rule(CheckCode::F404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F405.py"),
&settings::Settings::for_rule(CheckCode::F405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -359,12 +385,7 @@ mod tests {
fn f406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F406.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F406]),
},
&settings::Settings::for_rule(CheckCode::F406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -376,12 +397,7 @@ mod tests {
fn f407() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F407.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F407]),
},
&settings::Settings::for_rule(CheckCode::F407),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -393,12 +409,7 @@ mod tests {
fn f541() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F541.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F541]),
},
&settings::Settings::for_rule(CheckCode::F541),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -410,12 +421,7 @@ mod tests {
fn f601() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F601.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F601]),
},
&settings::Settings::for_rule(CheckCode::F601),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -427,12 +433,7 @@ mod tests {
fn f602() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F602.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F602]),
},
&settings::Settings::for_rule(CheckCode::F602),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -444,12 +445,7 @@ mod tests {
fn f622() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F622.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F622]),
},
&settings::Settings::for_rule(CheckCode::F622),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -461,12 +457,7 @@ mod tests {
fn f631() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F631.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F631]),
},
&settings::Settings::for_rule(CheckCode::F631),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -478,12 +469,7 @@ mod tests {
fn f632() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F632.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F632]),
},
&settings::Settings::for_rule(CheckCode::F632),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -495,12 +481,7 @@ mod tests {
fn f633() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F633.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F633]),
},
&settings::Settings::for_rule(CheckCode::F633),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -512,12 +493,7 @@ mod tests {
fn f634() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F634.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F634]),
},
&settings::Settings::for_rule(CheckCode::F634),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -529,12 +505,7 @@ mod tests {
fn f701() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F701.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F701]),
},
&settings::Settings::for_rule(CheckCode::F701),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -546,12 +517,7 @@ mod tests {
fn f702() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F702.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F702]),
},
&settings::Settings::for_rule(CheckCode::F702),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -563,12 +529,7 @@ mod tests {
fn f704() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F704.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F704]),
},
&settings::Settings::for_rule(CheckCode::F704),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -580,12 +541,7 @@ mod tests {
fn f706() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F706.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F706]),
},
&settings::Settings::for_rule(CheckCode::F706),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -597,12 +553,7 @@ mod tests {
fn f707() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F707.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F707]),
},
&settings::Settings::for_rule(CheckCode::F707),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -614,12 +565,7 @@ mod tests {
fn f722() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F722.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F722]),
},
&settings::Settings::for_rule(CheckCode::F722),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -631,12 +577,7 @@ mod tests {
fn f821() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F821.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821]),
},
&settings::Settings::for_rule(CheckCode::F821),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -648,12 +589,7 @@ mod tests {
fn f822() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F822.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F822]),
},
&settings::Settings::for_rule(CheckCode::F822),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -665,12 +601,7 @@ mod tests {
fn f823() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F823.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F823]),
},
&settings::Settings::for_rule(CheckCode::F823),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -682,12 +613,7 @@ mod tests {
fn f831() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F831.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F831]),
},
&settings::Settings::for_rule(CheckCode::F831),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -697,13 +623,23 @@ mod tests {
#[test]
fn f841() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings::for_rule(CheckCode::F841),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F841]),
dummy_variable_rgx: Regex::new(r"^z$").unwrap(),
..settings::Settings::for_rule(CheckCode::F841)
},
&fixer::Mode::Generate,
)?;
@@ -716,12 +652,19 @@ mod tests {
fn f901() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F901.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F901]),
},
&settings::Settings::for_rule(CheckCode::F901),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -733,12 +676,7 @@ mod tests {
fn r001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R001.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::R001]),
},
&settings::Settings::for_rule(CheckCode::R001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -750,12 +688,7 @@ mod tests {
fn r002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/R002.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::R002]),
},
&settings::Settings::for_rule(CheckCode::R002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -767,12 +700,7 @@ mod tests {
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821, CheckCode::F822]),
},
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -784,12 +712,67 @@ mod tests {
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F401, CheckCode::F821]),
},
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e999() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E999.py"),
&settings::Settings::for_rule(CheckCode::E999),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A001.py"),
&settings::Settings::for_rule(CheckCode::A001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A002.py"),
&settings::Settings::for_rule(CheckCode::A002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a003() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A003.py"),
&settings::Settings::for_rule(CheckCode::A003),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn spr001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/SPR001.py"),
&settings::Settings::for_rule(CheckCode::SPR001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);

View File

@@ -1,70 +1,95 @@
use std::path::PathBuf;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::{command, Parser};
use colored::Colorize;
use glob::Pattern;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use regex::Regex;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::add_noqa_to_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::settings::Settings;
use ::ruff::pyproject::{self, StrCheckCodePair};
use ::ruff::settings::CurrentSettings;
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ::ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[clap(name = format!("{CARGO_PKG_NAME} (v{CARGO_PKG_VERSION})"))]
#[clap(about = "An extremely fast Python linter.", long_about = None)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
#[arg(required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[clap(short, long, action)]
#[arg(short, long)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[clap(short, long, action)]
#[arg(short, long)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[clap(short, long, action)]
#[arg(short, long)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[clap(short, long, action)]
#[arg(short, long)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[clap(short, long, action)]
#[arg(short, long)]
fix: bool,
/// Disable cache reads.
#[clap(short, long, action)]
#[arg(short, long)]
no_cache: bool,
/// List of error codes to enable.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
exclude: Vec<Pattern>,
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
extend_exclude: Vec<Pattern>,
#[arg(long, value_delimiter = ',')]
extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[arg(long)]
show_files: bool,
/// See ruff's settings.
#[arg(long)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
add_noqa: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
dummy_variable_rgx: Option<Regex>,
}
#[cfg(feature = "update-informer")]
@@ -91,6 +116,22 @@ fn check_for_updates() {
}
}
fn show_settings(settings: Settings) {
println!("{:#?}", CurrentSettings::from_settings(settings));
}
fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
@@ -99,7 +140,7 @@ fn run_once(
) -> Result<Vec<Message>> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<DirEntry> = files
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
@@ -110,16 +151,34 @@ fn run_once(
let mut messages: Vec<Message> = paths
.par_iter()
.map(|entry| {
lint_path(entry.path(), settings, &cache.into(), &autofix.into()).unwrap_or_else(|e| {
if settings.select.contains(&CheckCode::E999) {
vec![Message {
kind: CheckKind::SyntaxError(e.to_string()),
fixed: false,
location: Default::default(),
filename: entry.path().to_string_lossy().to_string(),
}]
match entry {
Ok(entry) => {
let path = entry.path();
lint_path(path, settings, &cache.into(), &autofix.into())
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = path {
if settings.select.contains(&CheckCode::E902) {
vec![Message {
kind: CheckKind::IOError(message),
fixed: false,
location: Default::default(),
end_location: Default::default(),
filename: path.to_string_lossy().to_string(),
}]
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
vec![]
}
} else {
error!("Failed to check {}: {e:?}", entry.path().to_string_lossy());
error!("{message}");
vec![]
}
})
@@ -127,19 +186,6 @@ fn run_once(
.flatten()
.collect();
if settings.select.contains(&CheckCode::E902) {
for file in files {
if !file.exists() {
messages.push(Message {
kind: CheckKind::IOError(file.to_string_lossy().to_string()),
fixed: false,
location: Default::default(),
filename: file.to_string_lossy().to_string(),
})
}
}
}
messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
@@ -147,37 +193,123 @@ fn run_once(
Ok(messages)
}
fn add_noqa(files: &[PathBuf], settings: &Settings) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = paths
.par_iter()
.map(|entry| match entry {
Ok(entry) => {
let path = entry.path();
add_noqa_to_path(path, settings)
}
Err(_) => Ok(0),
})
.flatten()
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
Ok(modifications)
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
let mut settings = Settings::from_paths(&cli.files);
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&cli.files);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let mut printer = Printer::new(cli.format);
// Parse the settings from the pyproject.toml and command-line arguments.
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let per_file_ignores: Vec<PerFileIgnore> = cli
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = Settings::from_pyproject(pyproject, project_root)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
}
if !per_file_ignores.is_empty() {
settings.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
settings.clear();
settings.select(cli.select);
}
if !cli.extend_select.is_empty() {
settings.select(cli.extend_select);
}
if !cli.ignore.is_empty() {
settings.ignore(&cli.ignore);
}
if !cli.exclude.is_empty() {
settings.exclude = cli.exclude;
if !cli.extend_ignore.is_empty() {
settings.ignore(&cli.extend_ignore);
}
if !cli.extend_exclude.is_empty() {
settings.extend_exclude = cli.extend_exclude;
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
}
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
}
if cli.show_files {
show_files(&cli.files, &settings);
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
show_settings(settings);
return Ok(ExitCode::SUCCESS);
}
cache::init()?;
let mut printer = Printer::new(cli.format, cli.verbose);
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.");
eprintln!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
eprintln!("Warning: --no-qa is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
eprintln!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly.
@@ -214,6 +346,11 @@ fn inner_main() -> Result<ExitCode> {
Err(e) => return Err(e.into()),
}
}
} else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings)?;
if modifications > 0 {
println!("Added {modifications} noqa directives.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
@@ -234,6 +371,9 @@ fn inner_main() -> Result<ExitCode> {
fn main() -> ExitCode {
match inner_main() {
Ok(code) => code,
Err(_) => ExitCode::FAILURE,
Err(err) => {
eprintln!("{} {:?}", "error".red().bold(), err);
ExitCode::FAILURE
}
}
}

View File

@@ -1,17 +1,20 @@
use std::cmp::Ordering;
use std::fmt;
use std::path::Path;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
use crate::fs::relativize_path;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub kind: CheckKind,
pub fixed: bool,
pub location: Location,
pub end_location: Location,
pub filename: String,
}
@@ -36,7 +39,7 @@ impl fmt::Display for Message {
write!(
f,
"{}{}{}{}{}{} {} {}",
self.filename.white().bold(),
relativize_path(Path::new(&self.filename)).white().bold(),
":".cyan(),
self.location.row(),
":".cyan(),

298
src/noqa.rs Normal file
View File

@@ -0,0 +1,298 @@
use std::cmp::{max, min};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
.expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
#[derive(Debug)]
pub enum Directive<'a> {
None,
All(usize, usize),
Codes(usize, usize, Vec<&'a str>),
}
pub fn extract_noqa_directive(line: &str) -> Directive {
match NO_QA_REGEX.captures(line) {
Some(caps) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => Directive::Codes(
noqa.start(),
noqa.end(),
SPLIT_COMMA_REGEX
.split(codes.as_str())
.map(|code| code.trim())
.filter(|code| !code.is_empty())
.collect(),
),
None => Directive::All(noqa.start(), noqa.end()),
},
None => Directive::None,
},
None => Directive::None,
}
}
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> Vec<usize> {
let mut noqa_line_for: Vec<usize> = vec![];
let mut last_is_string = false;
let mut last_seen = usize::MIN;
let mut min_line = usize::MAX;
let mut max_line = usize::MIN;
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
}
if matches!(tok, Tok::Newline) {
min_line = min(min_line, start.row());
max_line = max(max_line, start.row());
// For now, we only care about preserving noqa directives across multi-line strings.
if last_is_string {
noqa_line_for.extend(vec![max_line; (max_line + 1) - min_line]);
} else {
for i in (min_line - 1)..(max_line) {
noqa_line_for.push(i + 1);
}
}
min_line = usize::MAX;
max_line = usize::MIN;
} else {
// Handle empty lines.
if start.row() > last_seen {
for i in last_seen..(start.row() - 1) {
noqa_line_for.push(i + 1);
}
}
min_line = min(min_line, start.row());
max_line = max(max_line, end.row());
}
last_seen = start.row();
last_is_string = matches!(tok, Tok::String { .. });
}
noqa_line_for
}
fn add_noqa_inner(
checks: &Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
) -> Result<(usize, String)> {
let lines: Vec<&str> = contents.lines().collect();
let mut matches_by_line: BTreeMap<usize, BTreeSet<&CheckCode>> = BTreeMap::new();
for lineno in 0..lines.len() {
let mut codes: BTreeSet<&CheckCode> = BTreeSet::new();
for check in checks {
if check.location.row() == lineno + 1 {
codes.insert(check.kind.code());
}
}
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if !codes.is_empty() {
let matches = matches_by_line
.entry(noqa_lineno)
.or_insert_with(BTreeSet::new);
matches.append(&mut codes);
}
}
let mut count: usize = 0;
let mut output = "".to_string();
for (lineno, line) in lines.iter().enumerate() {
match matches_by_line.get(&lineno) {
None => {
output.push_str(line);
output.push('\n');
}
Some(codes) => {
match extract_noqa_directive(line) {
Directive::None => {
output.push_str(line);
}
Directive::All(start, _) => output.push_str(&line[..start]),
Directive::Codes(start, _, _) => output.push_str(&line[..start]),
};
let codes: Vec<&str> = codes.iter().map(|code| code.as_str()).collect();
output.push_str(" # noqa: ");
output.push_str(&codes.join(", "));
output.push('\n');
count += 1;
}
}
}
Ok((count, output))
}
pub fn add_noqa(
checks: &Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
path: &Path,
) -> Result<usize> {
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
fs::write(path, output)?;
Ok(count)
}
#[cfg(test)]
mod tests {
use crate::ast::types::Range;
use anyhow::Result;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::checks::{Check, CheckKind};
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};
#[test]
fn extraction() -> Result<()> {
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"
x = 1
y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3, 4]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 2, 3, 4]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = '''abc
def
ghi
'''
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), vec![4, 4, 4, 4, 5, 6]);
Ok(())
}
#[test]
fn modification() -> Result<()> {
let checks = vec![];
let contents = "x = 1";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 0);
assert_eq!(output.trim(), contents.trim());
let checks = vec![Check::new(
CheckKind::UnusedVariable("x".to_string()),
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
)];
let contents = "x = 1";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: F841".trim());
let checks = vec![
Check::new(
CheckKind::AmbiguousVariableName("x".to_string()),
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
),
Check::new(
CheckKind::UnusedVariable("x".to_string()),
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
),
];
let contents = "x = 1 # noqa: E741";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
let checks = vec![
Check::new(
CheckKind::AmbiguousVariableName("x".to_string()),
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
),
Check::new(
CheckKind::UnusedVariable("x".to_string()),
Range {
location: Location::new(1, 1),
end_location: Location::new(1, 1),
},
),
];
let contents = "x = 1 # noqa";
let noqa_line_for = vec![1];
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
Ok(())
}
}

View File

@@ -1,8 +1,10 @@
use colored::Colorize;
use anyhow::Result;
use clap::ValueEnum;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::Serialize;
use crate::checks::{CheckCode, CheckKind};
use crate::message::Message;
use crate::tell_user;
@@ -12,13 +14,25 @@ pub enum SerializationFormat {
Json,
}
#[derive(Serialize)]
struct ExpandedMessage<'a> {
kind: &'a CheckKind,
code: &'a CheckCode,
message: String,
fixed: bool,
location: Location,
end_location: Location,
filename: &'a String,
}
pub struct Printer {
format: SerializationFormat,
verbose: bool,
}
impl Printer {
pub fn new(format: SerializationFormat) -> Self {
Self { format }
pub fn new(format: SerializationFormat, verbose: bool) -> Self {
Self { format, verbose }
}
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
@@ -31,7 +45,23 @@ impl Printer {
match self.format {
SerializationFormat::Json => {
println!("{}", serde_json::to_string_pretty(&messages)?)
println!(
"{}",
serde_json::to_string_pretty(
&messages
.iter()
.map(|m| ExpandedMessage {
kind: &m.kind,
code: m.kind.code(),
message: m.kind.body(),
fixed: m.fixed,
location: m.location,
end_location: m.end_location,
filename: &m.filename,
})
.collect::<Vec<_>>()
)?
)
}
SerializationFormat::Text => {
if !fixed.is_empty() {
@@ -40,7 +70,7 @@ impl Printer {
outstanding.len(),
fixed.len()
)
} else {
} else if !outstanding.is_empty() || self.verbose {
println!("Found {} error(s).", outstanding.len())
}

View File

@@ -1,33 +1,26 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result;
use anyhow::{anyhow, Result};
use common_path::common_path_all;
use log::debug;
use serde::Deserialize;
use path_absolutize::Absolutize;
use serde::de;
use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config(paths: &[PathBuf]) -> Config {
match find_project_root(paths) {
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {:?}", path);
match parse_pyproject_toml(&path) {
Ok(pyproject) => pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default(),
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Default::default()
}
}
}
None => Default::default(),
},
None => Default::default(),
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
Ok(Default::default())
}
}
}
@@ -35,10 +28,54 @@ pub fn load_config(paths: &[PathBuf]) -> Config {
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub extend_exclude: Option<Vec<PathBuf>>,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Option<Vec<String>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
pub per_file_ignores: Option<Vec<StrCheckCodePair>>,
pub dummy_variable_rgx: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCode,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0], tokens[1])
};
let code = CheckCode::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
@@ -56,20 +93,34 @@ fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
toml::from_str(&contents).map_err(|e| e.into())
}
fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
}
}
find_user_pyproject_toml()
}
fn find_user_pyproject_toml() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".ruff"))
let mut path = dirs::config_dir()?;
path.push("ruff");
path.push("pyproject.toml");
if path.is_file() {
Some(path)
} else {
None
}
}
fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
if let Some(prefix) = common_path_all(sources.iter().map(PathBuf::as_path)) {
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let absolute_sources: Vec<PathBuf> = sources
.iter()
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
.collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
@@ -88,10 +139,13 @@ fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::env::current_dir;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
use super::StrCheckCodePair;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
@@ -124,6 +178,8 @@ mod tests {
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -144,6 +200,8 @@ line-length = 79
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -160,10 +218,12 @@ exclude = ["foo.py"]
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -184,6 +244,8 @@ select = ["E501"]
extend_exclude: None,
select: Some(vec![CheckCode::E501]),
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -204,6 +266,8 @@ ignore = ["E501"]
extend_exclude: None,
select: None,
ignore: Some(vec![CheckCode::E501]),
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -241,13 +305,15 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?;
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, Path::new("resources/test/fixtures"));
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let path = find_pyproject_toml(&project_root).expect("Unable to find pyproject.toml.");
assert_eq!(path, Path::new("resources/test/fixtures/pyproject.toml"));
let path =
find_pyproject_toml(&Some(project_root)).expect("Unable to find pyproject.toml.");
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
@@ -260,58 +326,35 @@ other-attribute = 1
line_length: Some(88),
exclude: None,
extend_exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("migrations").to_path_buf(),
Path::new("./resources/test/fixtures/directory/also_excluded.py").to_path_buf()
]),
select: Some(vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F403,
CheckCode::F404,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
"excluded.py".to_string(),
"migrations".to_string(),
"directory/also_excluded.py".to_string(),
]),
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
}
);
Ok(())
}
#[test]
fn str_check_code_pair_strings() {
let result = StrCheckCodePair::from_str("foo:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("E501:foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("E501");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("foo:E501:E402");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("**/bar:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("bar:E502");
assert!(result.is_err());
}
}

View File

@@ -1,6 +1,7 @@
use once_cell::sync::Lazy;
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
static ANNOTATED_SUBSCRIPTS: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"AbstractAsyncContextManager",

View File

@@ -1,57 +1,141 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::checks::CheckCode;
use crate::pyproject::load_config;
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::{load_config, StrCheckCodePair};
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub code: CheckCode,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let code = user_in.code;
Self { pattern, code }
}
}
#[derive(Debug)]
pub struct Settings {
pub pyproject: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub line_length: usize,
pub exclude: Vec<Pattern>,
pub extend_exclude: Vec<Pattern>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub select: BTreeSet<CheckCode>,
pub per_file_ignores: Vec<PerFileIgnore>,
pub dummy_variable_rgx: Regex,
}
impl Settings {
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([check_code]),
per_file_ignores: vec![],
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
pyproject: None,
project_root: None,
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(check_codes),
per_file_ignores: vec![],
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.select.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
static DEFAULT_EXCLUDE: Lazy<Vec<Pattern>> = Lazy::new(|| {
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
Pattern::new(".bzr").unwrap(),
Pattern::new(".direnv").unwrap(),
Pattern::new(".eggs").unwrap(),
Pattern::new(".git").unwrap(),
Pattern::new(".hg").unwrap(),
Pattern::new(".mypy_cache").unwrap(),
Pattern::new(".nox").unwrap(),
Pattern::new(".pants.d").unwrap(),
Pattern::new(".ruff_cache").unwrap(),
Pattern::new(".svn").unwrap(),
Pattern::new(".tox").unwrap(),
Pattern::new(".venv").unwrap(),
Pattern::new("__pypackages__").unwrap(),
Pattern::new("_build").unwrap(),
Pattern::new("buck-out").unwrap(),
Pattern::new("build").unwrap(),
Pattern::new("dist").unwrap(),
Pattern::new("node_modules").unwrap(),
Pattern::new("venv").unwrap(),
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Settings {
pub fn from_paths(paths: &[PathBuf]) -> Self {
let config = load_config(paths);
pub fn from_pyproject(
pyproject: Option<PathBuf>,
project_root: Option<PathBuf>,
) -> Result<Self> {
let config = load_config(&pyproject)?;
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
@@ -59,9 +143,7 @@ impl Settings {
.map(|paths| {
paths
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
@@ -70,67 +152,43 @@ impl Settings {
.map(|paths| {
paths
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.map(|path| FilePattern::from_user(path, &project_root))
.collect()
})
.unwrap_or_default(),
select: BTreeSet::from_iter(config.select.unwrap_or_else(|| {
vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F403,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
// Disable refactoring codes by default.
// CheckCode::R001,
// CheckCode::R002,
]
})),
select: if let Some(select) = config.select {
BTreeSet::from_iter(select)
} else {
BTreeSet::from_iter(DEFAULT_CHECK_CODES)
},
per_file_ignores: config
.per_file_ignores
.map(|ignore_strings| {
ignore_strings
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect()
})
.unwrap_or_default(),
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
pyproject,
project_root,
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
settings
Ok(settings)
}
pub fn clear(&mut self) {
self.select.clear();
}
pub fn select(&mut self, codes: Vec<CheckCode>) {
self.select.clear();
for code in codes {
self.select.insert(code);
}
@@ -142,3 +200,62 @@ impl Settings {
}
}
}
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing Settings.
#[derive(Debug)]
pub struct CurrentSettings {
pub pyproject: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub line_length: usize,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub select: BTreeSet<CheckCode>,
pub per_file_ignores: Vec<PerFileIgnore>,
pub dummy_variable_rgx: Regex,
}
impl CurrentSettings {
pub fn from_settings(settings: Settings) -> Self {
Self {
pyproject: settings.pyproject,
project_root: settings.project_root,
line_length: settings.line_length,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
select: settings.select,
per_file_ignores: settings.per_file_ignores,
dummy_variable_rgx: settings.dummy_variable_rgx,
}
}
}

View File

@@ -0,0 +1,167 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BuiltinVariableShadowing: sum
location:
row: 1
column: 1
end_location:
row: 1
column: 19
fix: ~
- kind:
BuiltinVariableShadowing: int
location:
row: 2
column: 1
end_location:
row: 2
column: 30
fix: ~
- kind:
BuiltinVariableShadowing: print
location:
row: 4
column: 1
end_location:
row: 4
column: 6
fix: ~
- kind:
BuiltinVariableShadowing: copyright
location:
row: 5
column: 1
end_location:
row: 5
column: 10
fix: ~
- kind:
BuiltinVariableShadowing: complex
location:
row: 6
column: 2
end_location:
row: 6
column: 14
fix: ~
- kind:
BuiltinVariableShadowing: float
location:
row: 7
column: 1
end_location:
row: 7
column: 6
fix: ~
- kind:
BuiltinVariableShadowing: object
location:
row: 7
column: 9
end_location:
row: 7
column: 15
fix: ~
- kind:
BuiltinVariableShadowing: min
location:
row: 8
column: 1
end_location:
row: 8
column: 4
fix: ~
- kind:
BuiltinVariableShadowing: max
location:
row: 8
column: 6
end_location:
row: 8
column: 9
fix: ~
- kind:
BuiltinVariableShadowing: bytes
location:
row: 10
column: 1
end_location:
row: 13
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: slice
location:
row: 13
column: 1
end_location:
row: 16
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: ValueError
location:
row: 18
column: 1
end_location:
row: 21
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: memoryview
location:
row: 21
column: 5
end_location:
row: 21
column: 15
fix: ~
- kind:
BuiltinVariableShadowing: bytearray
location:
row: 21
column: 18
end_location:
row: 21
column: 27
fix: ~
- kind:
BuiltinVariableShadowing: str
location:
row: 24
column: 22
end_location:
row: 24
column: 25
fix: ~
- kind:
BuiltinVariableShadowing: all
location:
row: 24
column: 45
end_location:
row: 24
column: 48
fix: ~
- kind:
BuiltinVariableShadowing: any
location:
row: 24
column: 50
end_location:
row: 24
column: 53
fix: ~
- kind:
BuiltinVariableShadowing: sum
location:
row: 27
column: 8
end_location:
row: 27
column: 11
fix: ~

View File

@@ -0,0 +1,68 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BuiltinArgumentShadowing: str
location:
row: 1
column: 11
end_location:
row: 1
column: 14
fix: ~
- kind:
BuiltinArgumentShadowing: type
location:
row: 1
column: 19
end_location:
row: 1
column: 23
fix: ~
- kind:
BuiltinArgumentShadowing: complex
location:
row: 1
column: 26
end_location:
row: 1
column: 33
fix: ~
- kind:
BuiltinArgumentShadowing: Exception
location:
row: 1
column: 35
end_location:
row: 1
column: 44
fix: ~
- kind:
BuiltinArgumentShadowing: getattr
location:
row: 1
column: 48
end_location:
row: 1
column: 55
fix: ~
- kind:
BuiltinArgumentShadowing: bytes
location:
row: 5
column: 17
end_location:
row: 5
column: 22
fix: ~
- kind:
BuiltinArgumentShadowing: float
location:
row: 9
column: 16
end_location:
row: 9
column: 21
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BuiltinAttributeShadowing: ImportError
location:
row: 2
column: 5
end_location:
row: 2
column: 16
fix: ~
- kind:
BuiltinAttributeShadowing: str
location:
row: 7
column: 5
end_location:
row: 9
column: 1
fix: ~

View File

@@ -4,7 +4,10 @@ expression: checks
---
- kind: ModuleImportNotAtTopOfFile
location:
row: 20
row: 24
column: 1
end_location:
row: 24
column: 9
fix: ~

View File

@@ -8,6 +8,9 @@ expression: checks
- 88
location:
row: 5
column: 89
column: 1
end_location:
row: 5
column: 124
fix: ~

View File

@@ -7,47 +7,71 @@ expression: checks
location:
row: 2
column: 11
end_location:
row: 2
column: 15
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 5
column: 11
end_location:
row: 5
column: 15
fix: ~
- kind:
NoneComparison: Eq
location:
row: 8
column: 4
end_location:
row: 8
column: 8
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 11
column: 4
end_location:
row: 11
column: 8
fix: ~
- kind:
NoneComparison: Eq
location:
row: 14
column: 14
end_location:
row: 14
column: 18
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 17
column: 14
end_location:
row: 17
column: 18
fix: ~
- kind:
NoneComparison: NotEq
location:
row: 20
column: 4
end_location:
row: 20
column: 8
fix: ~
- kind:
NoneComparison: Eq
location:
row: 23
column: 4
end_location:
row: 23
column: 8
fix: ~

View File

@@ -9,6 +9,9 @@ expression: checks
location:
row: 2
column: 11
end_location:
row: 2
column: 15
fix: ~
- kind:
TrueFalseComparison:
@@ -17,6 +20,9 @@ expression: checks
location:
row: 5
column: 11
end_location:
row: 5
column: 16
fix: ~
- kind:
TrueFalseComparison:
@@ -25,6 +31,9 @@ expression: checks
location:
row: 8
column: 4
end_location:
row: 8
column: 8
fix: ~
- kind:
TrueFalseComparison:
@@ -33,6 +42,9 @@ expression: checks
location:
row: 11
column: 4
end_location:
row: 11
column: 9
fix: ~
- kind:
TrueFalseComparison:
@@ -41,6 +53,9 @@ expression: checks
location:
row: 14
column: 14
end_location:
row: 14
column: 18
fix: ~
- kind:
TrueFalseComparison:
@@ -49,6 +64,9 @@ expression: checks
location:
row: 17
column: 14
end_location:
row: 17
column: 19
fix: ~
- kind:
TrueFalseComparison:
@@ -57,6 +75,9 @@ expression: checks
location:
row: 20
column: 20
end_location:
row: 20
column: 24
fix: ~
- kind:
TrueFalseComparison:
@@ -65,6 +86,9 @@ expression: checks
location:
row: 20
column: 44
end_location:
row: 20
column: 49
fix: ~
- kind:
TrueFalseComparison:
@@ -73,5 +97,8 @@ expression: checks
location:
row: 22
column: 5
end_location:
row: 22
column: 9
fix: ~

View File

@@ -6,25 +6,40 @@ expression: checks
location:
row: 2
column: 10
end_location:
row: 2
column: 14
fix: ~
- kind: NotInTest
location:
row: 5
column: 12
end_location:
row: 5
column: 16
fix: ~
- kind: NotInTest
location:
row: 8
column: 10
end_location:
row: 8
column: 14
fix: ~
- kind: NotInTest
location:
row: 11
column: 25
end_location:
row: 11
column: 29
fix: ~
- kind: NotInTest
location:
row: 14
column: 11
end_location:
row: 14
column: 15
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 2
column: 10
end_location:
row: 2
column: 14
fix: ~
- kind: NotIsTest
location:
row: 5
column: 12
end_location:
row: 5
column: 16
fix: ~
- kind: NotIsTest
location:
row: 8
column: 10
end_location:
row: 8
column: 23
fix: ~

View File

@@ -6,80 +6,128 @@ expression: checks
location:
row: 2
column: 14
end_location:
row: 2
column: 25
fix: ~
- kind: TypeComparison
location:
row: 5
column: 14
end_location:
row: 5
column: 25
fix: ~
- kind: TypeComparison
location:
row: 10
column: 8
end_location:
row: 10
column: 24
fix: ~
- kind: TypeComparison
location:
row: 15
column: 14
end_location:
row: 15
column: 35
fix: ~
- kind: TypeComparison
location:
row: 18
column: 18
end_location:
row: 18
column: 32
fix: ~
- kind: TypeComparison
location:
row: 18
column: 46
end_location:
row: 18
column: 59
fix: ~
- kind: TypeComparison
location:
row: 20
column: 18
end_location:
row: 20
column: 29
fix: ~
- kind: TypeComparison
location:
row: 22
column: 18
end_location:
row: 22
column: 29
fix: ~
- kind: TypeComparison
location:
row: 24
column: 18
end_location:
row: 24
column: 31
fix: ~
- kind: TypeComparison
location:
row: 26
column: 18
end_location:
row: 26
column: 30
fix: ~
- kind: TypeComparison
location:
row: 28
column: 18
end_location:
row: 28
column: 31
fix: ~
- kind: TypeComparison
location:
row: 30
column: 18
end_location:
row: 30
column: 31
fix: ~
- kind: TypeComparison
location:
row: 32
column: 18
end_location:
row: 32
column: 35
fix: ~
- kind: TypeComparison
location:
row: 34
column: 18
end_location:
row: 38
column: 2
fix: ~
- kind: TypeComparison
location:
row: 40
column: 18
end_location:
row: 40
column: 29
fix: ~
- kind: TypeComparison
location:
row: 42
column: 18
end_location:
row: 42
column: 31
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 4
column: 1
end_location:
row: 7
column: 1
fix: ~
- kind: DoNotUseBareExcept
location:
row: 11
column: 1
end_location:
row: 14
column: 1
fix: ~
- kind: DoNotUseBareExcept
location:
row: 16
column: 1
end_location:
row: 19
column: 1
fix: ~

View File

@@ -6,25 +6,40 @@ expression: checks
location:
row: 2
column: 1
end_location:
row: 2
column: 20
fix: ~
- kind: DoNotAssignLambda
location:
row: 4
column: 1
end_location:
row: 4
column: 20
fix: ~
- kind: DoNotAssignLambda
location:
row: 7
column: 5
end_location:
row: 7
column: 30
fix: ~
- kind: DoNotAssignLambda
location:
row: 12
column: 1
end_location:
row: 12
column: 28
fix: ~
- kind: DoNotAssignLambda
location:
row: 16
column: 1
end_location:
row: 16
column: 26
fix: ~

View File

@@ -7,149 +7,224 @@ expression: checks
location:
row: 3
column: 1
end_location:
row: 3
column: 2
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 4
column: 1
end_location:
row: 4
column: 2
fix: ~
- kind:
AmbiguousVariableName: O
location:
row: 5
column: 1
end_location:
row: 5
column: 2
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 6
column: 1
end_location:
row: 6
column: 2
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 8
column: 4
end_location:
row: 8
column: 5
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 9
column: 5
end_location:
row: 9
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 10
column: 5
end_location:
row: 10
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 11
column: 5
end_location:
row: 11
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 16
column: 5
end_location:
row: 16
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 20
column: 8
end_location:
row: 20
column: 9
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 25
column: 5
end_location:
row: 25
column: 13
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 26
column: 5
end_location:
row: 26
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 30
column: 5
end_location:
row: 30
column: 6
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 33
column: 9
end_location:
row: 33
column: 19
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 34
column: 9
end_location:
row: 34
column: 10
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 40
column: 8
end_location:
row: 40
column: 9
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 40
column: 14
end_location:
row: 40
column: 15
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 44
column: 8
end_location:
row: 44
column: 9
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 44
column: 16
end_location:
row: 44
column: 17
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 48
column: 9
end_location:
row: 48
column: 10
fix: ~
- kind:
AmbiguousVariableName: I
location:
row: 48
column: 14
end_location:
row: 48
column: 15
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 57
column: 16
end_location:
row: 57
column: 17
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 66
column: 20
end_location:
row: 66
column: 21
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 71
column: 1
end_location:
row: 74
column: 1
fix: ~
- kind:
AmbiguousVariableName: l
location:
row: 74
column: 5
end_location:
row: 74
column: 11
fix: ~

View File

@@ -7,17 +7,26 @@ expression: checks
location:
row: 1
column: 1
end_location:
row: 5
column: 1
fix: ~
- kind:
AmbiguousClassName: I
location:
row: 5
column: 1
end_location:
row: 9
column: 1
fix: ~
- kind:
AmbiguousClassName: O
location:
row: 9
column: 1
end_location:
row: 13
column: 1
fix: ~

View File

@@ -7,17 +7,26 @@ expression: checks
location:
row: 1
column: 1
end_location:
row: 5
column: 1
fix: ~
- kind:
AmbiguousFunctionName: I
location:
row: 5
column: 1
end_location:
row: 9
column: 1
fix: ~
- kind:
AmbiguousFunctionName: O
location:
row: 10
column: 5
end_location:
row: 14
column: 1
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
SyntaxError: Got unexpected EOF
location:
row: 2
column: 1
end_location:
row: 2
column: 1
fix: ~

View File

@@ -7,17 +7,26 @@ expression: checks
location:
row: 3
column: 1
end_location:
row: 3
column: 17
fix: ~
- kind:
UnusedImport: collections.OrderedDict
location:
row: 5
column: 1
end_location:
row: 9
column: 2
fix: ~
- kind:
UnusedImport: logging.handlers
location:
row: 13
column: 1
end_location:
row: 13
column: 24
fix: ~

View File

@@ -0,0 +1,27 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ImportShadowedByLoopVar:
- os
- 1
location:
row: 5
column: 5
end_location:
row: 5
column: 7
fix: ~
- kind:
ImportShadowedByLoopVar:
- path
- 2
location:
row: 8
column: 5
end_location:
row: 8
column: 9
fix: ~

View File

@@ -3,15 +3,21 @@ source: src/linter.rs
expression: checks
---
- kind:
ImportStarUsage: F634
ImportStarUsed: F634
location:
row: 1
column: 1
end_location:
row: 1
column: 19
fix: ~
- kind:
ImportStarUsage: F634
ImportStarUsed: F634
location:
row: 2
column: 1
end_location:
row: 2
column: 19
fix: ~

View File

@@ -6,5 +6,8 @@ expression: checks
location:
row: 7
column: 1
end_location:
row: 7
column: 38
fix: ~

View File

@@ -0,0 +1,27 @@
---
source: src/linter.rs
expression: checks
---
- kind:
ImportStarUsage:
- name
- mymodule
location:
row: 5
column: 11
end_location:
row: 5
column: 15
fix: ~
- kind:
ImportStarUsage:
- a
- mymodule
location:
row: 11
column: 1
end_location:
row: 11
column: 8
fix: ~

View File

@@ -7,11 +7,17 @@ expression: checks
location:
row: 5
column: 5
end_location:
row: 5
column: 23
fix: ~
- kind:
ImportStarNotPermitted: F634
location:
row: 9
column: 5
end_location:
row: 9
column: 23
fix: ~

View File

@@ -7,5 +7,8 @@ expression: checks
location:
row: 2
column: 1
end_location:
row: 2
column: 44
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 4
column: 7
end_location:
row: 4
column: 11
fix: ~
- kind: FStringMissingPlaceholders
location:
row: 5
column: 7
end_location:
row: 5
column: 11
fix: ~
- kind: FStringMissingPlaceholders
location:
row: 7
column: 7
end_location:
row: 7
column: 11
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 3
column: 6
end_location:
row: 3
column: 8
fix: ~
- kind: MultiValueRepeatedKeyLiteral
location:
row: 9
column: 5
end_location:
row: 9
column: 6
fix: ~
- kind: MultiValueRepeatedKeyLiteral
location:
row: 11
column: 7
end_location:
row: 11
column: 11
fix: ~

View File

@@ -7,5 +7,8 @@ expression: checks
location:
row: 5
column: 5
end_location:
row: 5
column: 6
fix: ~

View File

@@ -6,5 +6,8 @@ expression: checks
location:
row: 1
column: 1
end_location:
row: 1
column: 10
fix: ~

View File

@@ -6,10 +6,16 @@ expression: checks
location:
row: 1
column: 1
end_location:
row: 1
column: 20
fix: ~
- kind: AssertTuple
location:
row: 2
column: 1
end_location:
row: 2
column: 16
fix: ~

View File

@@ -6,10 +6,16 @@ expression: checks
location:
row: 1
column: 6
end_location:
row: 1
column: 14
fix: ~
- kind: IsLiteral
location:
row: 4
column: 8
end_location:
row: 4
column: 16
fix: ~

View File

@@ -6,5 +6,8 @@ expression: checks
location:
row: 4
column: 1
end_location:
row: 4
column: 6
fix: ~

View File

@@ -6,10 +6,16 @@ expression: checks
location:
row: 1
column: 1
end_location:
row: 4
column: 1
fix: ~
- kind: IfTuple
location:
row: 7
column: 5
end_location:
row: 9
column: 5
fix: ~

View File

@@ -6,20 +6,32 @@ expression: checks
location:
row: 4
column: 5
end_location:
row: 4
column: 10
fix: ~
- kind: BreakOutsideLoop
location:
row: 16
column: 5
end_location:
row: 16
column: 10
fix: ~
- kind: BreakOutsideLoop
location:
row: 20
column: 5
end_location:
row: 20
column: 10
fix: ~
- kind: BreakOutsideLoop
location:
row: 23
column: 1
end_location:
row: 23
column: 6
fix: ~

View File

@@ -6,20 +6,32 @@ expression: checks
location:
row: 4
column: 5
end_location:
row: 4
column: 13
fix: ~
- kind: ContinueOutsideLoop
location:
row: 16
column: 5
end_location:
row: 16
column: 13
fix: ~
- kind: ContinueOutsideLoop
location:
row: 20
column: 5
end_location:
row: 20
column: 13
fix: ~
- kind: ContinueOutsideLoop
location:
row: 23
column: 1
end_location:
row: 23
column: 9
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 6
column: 5
end_location:
row: 6
column: 12
fix: ~
- kind: YieldOutsideFunction
location:
row: 9
column: 1
end_location:
row: 9
column: 8
fix: ~
- kind: YieldOutsideFunction
location:
row: 10
column: 1
end_location:
row: 10
column: 13
fix: ~

View File

@@ -6,10 +6,16 @@ expression: checks
location:
row: 6
column: 5
end_location:
row: 6
column: 13
fix: ~
- kind: ReturnOutsideFunction
location:
row: 9
column: 1
end_location:
row: 9
column: 9
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 3
column: 1
end_location:
row: 5
column: 1
fix: ~
- kind: DefaultExceptNotLast
location:
row: 10
column: 1
end_location:
row: 12
column: 1
fix: ~
- kind: DefaultExceptNotLast
location:
row: 19
column: 1
end_location:
row: 21
column: 1
fix: ~

View File

@@ -7,5 +7,8 @@ expression: checks
location:
row: 9
column: 13
end_location:
row: 9
column: 17
fix: ~

View File

@@ -7,35 +7,71 @@ expression: checks
location:
row: 2
column: 12
end_location:
row: 2
column: 16
fix: ~
- kind:
UndefinedName: self
location:
row: 6
column: 13
end_location:
row: 6
column: 17
fix: ~
- kind:
UndefinedName: self
location:
row: 10
column: 9
end_location:
row: 10
column: 13
fix: ~
- kind:
UndefinedName: numeric_string
location:
row: 21
column: 12
end_location:
row: 21
column: 26
fix: ~
- kind:
UndefinedName: Bar
location:
row: 58
column: 5
end_location:
row: 58
column: 9
fix: ~
- kind:
UndefinedName: TOMATO
location:
row: 83
column: 11
end_location:
row: 83
column: 17
fix: ~
- kind:
UndefinedName: B
location:
row: 87
column: 7
end_location:
row: 87
column: 11
fix: ~
- kind:
UndefinedName: B
location:
row: 89
column: 7
end_location:
row: 89
column: 9
fix: ~

View File

@@ -7,5 +7,8 @@ expression: checks
location:
row: 3
column: 1
end_location:
row: 3
column: 8
fix: ~

View File

@@ -7,5 +7,8 @@ expression: checks
location:
row: 6
column: 5
end_location:
row: 6
column: 11
fix: ~

View File

@@ -6,15 +6,24 @@ expression: checks
location:
row: 1
column: 25
end_location:
row: 1
column: 31
fix: ~
- kind: DuplicateArgumentName
location:
row: 5
column: 28
end_location:
row: 5
column: 34
fix: ~
- kind: DuplicateArgumentName
location:
row: 9
column: 27
end_location:
row: 9
column: 33
fix: ~

View File

@@ -7,29 +7,44 @@ expression: checks
location:
row: 3
column: 1
end_location:
row: 7
column: 1
fix: ~
- kind:
UnusedVariable: z
location:
row: 16
column: 5
end_location:
row: 16
column: 6
fix: ~
- kind:
UnusedVariable: foo
location:
row: 20
column: 5
end_location:
row: 20
column: 8
fix: ~
- kind:
UnusedVariable: a
location:
row: 21
column: 6
end_location:
row: 21
column: 7
fix: ~
- kind:
UnusedVariable: b
location:
row: 21
column: 9
end_location:
row: 21
column: 10
fix: ~

View File

@@ -0,0 +1,68 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedVariable: e
location:
row: 3
column: 1
end_location:
row: 7
column: 1
fix: ~
- kind:
UnusedVariable: foo
location:
row: 20
column: 5
end_location:
row: 20
column: 8
fix: ~
- kind:
UnusedVariable: a
location:
row: 21
column: 6
end_location:
row: 21
column: 7
fix: ~
- kind:
UnusedVariable: b
location:
row: 21
column: 9
end_location:
row: 21
column: 10
fix: ~
- kind:
UnusedVariable: _
location:
row: 35
column: 5
end_location:
row: 35
column: 6
fix: ~
- kind:
UnusedVariable: __
location:
row: 36
column: 5
end_location:
row: 36
column: 7
fix: ~
- kind:
UnusedVariable: _discarded
location:
row: 37
column: 5
end_location:
row: 37
column: 15
fix: ~

View File

@@ -5,11 +5,17 @@ expression: checks
- kind: RaiseNotImplemented
location:
row: 2
column: 25
column: 11
end_location:
row: 2
column: 27
fix: ~
- kind: RaiseNotImplemented
location:
row: 6
column: 11
end_location:
row: 6
column: 25
fix: ~

View File

@@ -7,11 +7,17 @@ expression: checks
location:
row: 5
column: 1
end_location:
row: 5
column: 30
fix: ~
- kind:
UndefinedName: Bar
location:
row: 22
column: 19
end_location:
row: 22
column: 22
fix: ~

View File

@@ -0,0 +1,107 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedNOQA: ~
location:
row: 9
column: 10
end_location:
row: 9
column: 18
fix:
content: ""
location:
row: 9
column: 10
end_location:
row: 9
column: 18
applied: false
- kind:
UnusedNOQA: E501
location:
row: 13
column: 10
end_location:
row: 13
column: 24
fix:
content: ""
location:
row: 13
column: 10
end_location:
row: 13
column: 24
applied: false
- kind:
UnusedNOQA: E501
location:
row: 16
column: 10
end_location:
row: 16
column: 30
fix:
content: " # noqa: F841"
location:
row: 16
column: 10
end_location:
row: 16
column: 30
applied: false
- kind:
UnusedNOQA: F841
location:
row: 41
column: 4
end_location:
row: 41
column: 24
fix:
content: " # noqa: E501"
location:
row: 41
column: 4
end_location:
row: 41
column: 24
applied: false
- kind:
UnusedNOQA: E501
location:
row: 49
column: 4
end_location:
row: 49
column: 18
fix:
content: ""
location:
row: 49
column: 4
end_location:
row: 49
column: 18
applied: false
- kind:
UnusedNOQA: ~
location:
row: 57
column: 4
end_location:
row: 57
column: 12
fix:
content: ""
location:
row: 57
column: 4
end_location:
row: 57
column: 12
applied: false

View File

@@ -7,12 +7,15 @@ expression: checks
location:
row: 5
column: 9
end_location:
row: 5
column: 15
fix:
content: ""
start:
location:
row: 5
column: 8
end:
end_location:
row: 5
column: 16
applied: false
@@ -21,12 +24,15 @@ expression: checks
location:
row: 10
column: 5
end_location:
row: 10
column: 11
fix:
content: ""
start:
location:
row: 9
column: 8
end:
end_location:
row: 11
column: 2
applied: false
@@ -35,12 +41,15 @@ expression: checks
location:
row: 16
column: 5
end_location:
row: 16
column: 11
fix:
content: ""
start:
location:
row: 15
column: 8
end:
end_location:
row: 18
column: 2
applied: false
@@ -49,12 +58,15 @@ expression: checks
location:
row: 24
column: 5
end_location:
row: 24
column: 11
fix:
content: ""
start:
location:
row: 22
column: 8
end:
end_location:
row: 25
column: 2
applied: false
@@ -63,12 +75,15 @@ expression: checks
location:
row: 31
column: 5
end_location:
row: 31
column: 11
fix:
content: ""
start:
location:
row: 29
column: 8
end:
end_location:
row: 32
column: 2
applied: false
@@ -77,12 +92,15 @@ expression: checks
location:
row: 37
column: 5
end_location:
row: 37
column: 11
fix:
content: ""
start:
location:
row: 36
column: 8
end:
end_location:
row: 39
column: 2
applied: false
@@ -91,12 +109,15 @@ expression: checks
location:
row: 45
column: 5
end_location:
row: 45
column: 11
fix:
content: ""
start:
location:
row: 43
column: 8
end:
end_location:
row: 47
column: 2
applied: false
@@ -105,12 +126,15 @@ expression: checks
location:
row: 53
column: 5
end_location:
row: 53
column: 11
fix:
content: ""
start:
location:
row: 51
column: 8
end:
end_location:
row: 55
column: 2
applied: false
@@ -119,12 +143,15 @@ expression: checks
location:
row: 61
column: 5
end_location:
row: 61
column: 11
fix:
content: ""
start:
location:
row: 59
column: 8
end:
end_location:
row: 63
column: 2
applied: false
@@ -133,12 +160,15 @@ expression: checks
location:
row: 69
column: 5
end_location:
row: 69
column: 11
fix:
content: ""
start:
location:
row: 67
column: 8
end:
end_location:
row: 71
column: 2
applied: false
@@ -147,12 +177,15 @@ expression: checks
location:
row: 75
column: 12
end_location:
row: 75
column: 18
fix:
content: ""
start:
location:
row: 75
column: 10
end:
end_location:
row: 75
column: 18
applied: false
@@ -161,12 +194,15 @@ expression: checks
location:
row: 79
column: 9
end_location:
row: 79
column: 15
fix:
content: ""
start:
location:
row: 79
column: 9
end:
end_location:
row: 79
column: 17
applied: false
@@ -175,12 +211,15 @@ expression: checks
location:
row: 84
column: 5
end_location:
row: 84
column: 11
fix:
content: ""
start:
location:
row: 84
column: 5
end:
end_location:
row: 85
column: 5
applied: false
@@ -189,12 +228,15 @@ expression: checks
location:
row: 92
column: 5
end_location:
row: 92
column: 11
fix:
content: ""
start:
location:
row: 91
column: 6
end:
end_location:
row: 92
column: 11
applied: false
@@ -203,12 +245,15 @@ expression: checks
location:
row: 98
column: 5
end_location:
row: 98
column: 11
fix:
content: ""
start:
location:
row: 98
column: 5
end:
end_location:
row: 100
column: 5
applied: false
@@ -217,12 +262,15 @@ expression: checks
location:
row: 108
column: 5
end_location:
row: 108
column: 11
fix:
content: ""
start:
location:
row: 107
column: 6
end:
end_location:
row: 108
column: 11
applied: false
@@ -231,12 +279,15 @@ expression: checks
location:
row: 114
column: 13
end_location:
row: 114
column: 19
fix:
content: ""
start:
location:
row: 114
column: 12
end:
end_location:
row: 114
column: 20
applied: false
@@ -245,12 +296,15 @@ expression: checks
location:
row: 119
column: 5
end_location:
row: 119
column: 11
fix:
content: ""
start:
location:
row: 118
column: 8
end:
end_location:
row: 120
column: 2
applied: false
@@ -259,12 +313,15 @@ expression: checks
location:
row: 125
column: 5
end_location:
row: 125
column: 11
fix:
content: ""
start:
location:
row: 124
column: 8
end:
end_location:
row: 126
column: 2
applied: false
@@ -273,12 +330,15 @@ expression: checks
location:
row: 131
column: 5
end_location:
row: 131
column: 11
fix:
content: ""
start:
location:
row: 130
column: 8
end:
end_location:
row: 133
column: 2
applied: false

View File

@@ -5,27 +5,33 @@ expression: checks
- kind: NoAssertEquals
location:
row: 1
column: 5
column: 1
end_location:
row: 1
column: 18
fix:
content: assertEqual
start:
location:
row: 1
column: 6
end:
column: 2
end_location:
row: 1
column: 18
column: 14
applied: false
- kind: NoAssertEquals
location:
row: 2
column: 5
column: 1
end_location:
row: 2
column: 18
fix:
content: assertEqual
start:
location:
row: 2
column: 6
end:
column: 2
end_location:
row: 2
column: 18
column: 14
applied: false

View File

@@ -0,0 +1,53 @@
---
source: src/linter.rs
expression: checks
---
- kind: SuperCallWithParameters
location:
row: 17
column: 18
end_location:
row: 17
column: 36
fix:
content: super()
location:
row: 17
column: 18
end_location:
row: 17
column: 36
applied: false
- kind: SuperCallWithParameters
location:
row: 18
column: 9
end_location:
row: 18
column: 27
fix:
content: super()
location:
row: 18
column: 9
end_location:
row: 18
column: 27
applied: false
- kind: SuperCallWithParameters
location:
row: 19
column: 9
end_location:
row: 22
column: 10
fix:
content: super()
location:
row: 19
column: 9
end_location:
row: 22
column: 10
applied: false