Compare commits

...

128 Commits

Author SHA1 Message Date
Charlie Marsh
e8810eae64 Fix version number in BREAKING_CHANGES.md 2023-01-30 18:49:11 -05:00
Charlie Marsh
ba26a60e2a Disable incompatible rules rather than merely warning (#2369)
This is another temporary fix for the problem described in #2289 and #2292. Rather than merely warning, we now disable the incompatible rules (in addition to the warning). I actually think this is quite a reasonable solution, but we can revisit later. I just can't bring myself to ship another release with autofix broken-by-default 😂
2023-01-30 18:47:05 -05:00
Charlie Marsh
42459c35b0 Update BREAKING_CHANGES.md 2023-01-30 17:50:26 -05:00
Charlie Marsh
1cbd929a0a Bump version to 0.0.238 2023-01-30 16:44:19 -05:00
Charlie Marsh
5f07e70762 Recommend disabling explicit-string-concatenation (#2366)
If `allow-multiline = false` is set, then if the user enables `explicit-string-concatenation` (`ISC003`), there's no way for them to create valid multiline strings. This PR notes that they should turn off `ISC003`.

Closes #2362.
2023-01-30 16:42:30 -05:00
Charlie Marsh
8963a62ec0 Refine criteria for exc_info logger rules (#2364)
We now only trigger `logging-exc-info` and `logging-redundant-exc-info` when in an exception handler, with an `exc_info` that isn't `true` or `sys.exc_info()`.

Closes #2356.
2023-01-30 16:32:00 -05:00
Charlie Marsh
4589daa0bd Ignore magic comparisons to bytes by default (#2365) 2023-01-30 16:31:48 -05:00
Charlie Marsh
ea0274d22c Use bold for deprecated 2023-01-30 16:28:21 -05:00
Charlie Marsh
ca1129ad27 Document new rule config resolution 2023-01-30 16:26:59 -05:00
Martin Fischer
104c63afc6 Exclude deprecated extend-ignore from the JSON schema
Now that the option is deprecated we no longer
want IDEs to suggest it in their autocompletion.
2023-01-30 16:26:59 -05:00
Martin Fischer
ba457c21b5 Improve rule config resolution
Ruff allows rules to be enabled with `select` and disabled with
`ignore`, where the more specific rule selector takes precedence,
for example:

    `--select ALL --ignore E501` selects all rules except E501
    `--ignore ALL --select E501` selects only E501

(If both selectors have the same specificity ignore selectors
take precedence.)

Ruff always had two quirks:

* If `pyproject.toml` specified `ignore = ["E501"]` then you could
  previously not override that with `--select E501` on the command-line
  (since the resolution didn't take into account that the select was
  specified after the ignore).

* If `pyproject.toml` specified `select = ["E501"]` then you could
  previously not override that with `--ignore E` on the command-line
  (since the resolution didn't take into account that the ignore was
  specified after the select).

Since d067efe265 (#1245)
`extend-select` and `extend-ignore` always override
`select` and `ignore` and are applied iteratively in pairs,
which introduced another quirk:

* If some `pyproject.toml` file specified `extend-select`
  or `extend-ignore`, `select` and `ignore` became pretty much
  unreliable after that with no way of resetting that.

This commit fixes all of these quirks by making later configuration
sources take precedence over earlier configuration sources.

While this is a breaking change, we expect most ruff configuration
files to not rely on the previous unintutive behavior.
2023-01-30 16:26:59 -05:00
Martin Fischer
a92958f941 Test that more specific select wins over less specific ignore 2023-01-30 16:26:59 -05:00
Martin Fischer
1cd206285e refactor: test impl From<&Configuration> for RuleTable
Previously we tested the resolve_codes helper function directly.
Since we want to rewrite our resolution logic in the next commit,
this commit changes the tests to test the more high-level From impl.
2023-01-30 16:26:59 -05:00
Samuel Cormier-Iijima
5ac5b69e9f [I001] fix isort for files with tab-based indentation (#2361)
This PR fixes two related issues with using isort on files using tabs for indentation:

- Multiline imports are never considered correctly formatted, since the comparison with the generated code will always fail.
- Using autofix generates code that can have mixed indentation in the same line, for imports that are within nested blocks.
2023-01-30 15:36:19 -05:00
Charlie Marsh
01fedec1e7 Add SciPy and meson-python (#2363) 2023-01-30 15:34:19 -05:00
Martin Fischer
ef20692149 fix: clap usage for CLI help generation in the README (#2358) 2023-01-30 13:14:40 -05:00
Simon Brugman
50046fbed3 Extend conventional imports defaults to include TensorFlow et al (#2353)
extend conventional imports

Based on configuration from Visual Studio for Python
(https://code.visualstudio.com/docs/python/editing#_quick-fixes)
2023-01-30 11:04:19 -05:00
Charlie Marsh
6798675db1 Avoid removing trailing comments when autofixing (#2352) 2023-01-30 07:44:20 -05:00
Akhil
8e5a944ce1 Implement Pylint's too-many-arguments rule (PLR0913) (#2308) 2023-01-30 07:34:37 -05:00
messense
1e325edfb1 Configure automatically generated release notes (#2341) 2023-01-30 07:21:29 -05:00
Simon Brugman
502574797f include tomllib in standard lib (#2345) 2023-01-30 06:59:59 -05:00
Charlie Marsh
7a83b65fbe Pre-allocate output contents during autofix application (#2340) 2023-01-29 22:40:27 -05:00
Charlie Marsh
74e3cdfd7c Add a dedicated single-fix helper (#2339) 2023-01-29 22:38:29 -05:00
Simon Brugman
2ef28f217c pandas vet autofix for PD002 and general refactor 2023-01-29 22:30:37 -05:00
Simon Brugman
63fc912ed8 refactor: use remove_argument helper in pyupgrade 2023-01-29 22:30:37 -05:00
Martin Fischer
d76a47d366 Implement ruff linter subcommand
The subcommand lists all supported upstream linters and their prefixes:

    $ ruff linter
       F Pyflakes
     E/W pycodestyle
     C90 mccabe
       I isort
       N pep8-naming
       D pydocstyle
      UP pyupgrade
     YTT flake8-2020
    # etc...

Just like with the `rule` subcommand `--format json` is supported:

    $ ruff linter --format json
    [
      {
        "prefix": "F",
        "name": "Pyflakes"
      },
      {
        "prefix": "",
        "name": "pycodestyle",
        "categories": [
          {
            "prefix": "E",
            "name": "Error"
          },
          {
            "prefix": "W",
            "name": "Warning"
          }
        ]
      },
      # etc...
2023-01-29 21:32:37 -05:00
Martin Fischer
b532fce792 refactor: Change RuleNamespace::prefixes to common_prefix
Previously Linter::parse_code("E401") returned
(Linter::Pycodestyle, "401") ... after this commit it returns
(Linter::Pycodestyle, "E401") instead, which is important
for the future implementation of the many-to-many mapping.
(The second value of the tuple isn't used currently.)
2023-01-29 21:32:37 -05:00
Charlie Marsh
3ee6a90905 Remove remove-six-compat (UP016) (#2332) 2023-01-29 21:19:59 -05:00
Samuel Cormier-Iijima
0a6d2294a7 [TRY201] don't check raise statements in nested exception handlers (#2337) 2023-01-29 21:16:18 -05:00
Simon Brugman
e66fb42d0b refactor: use patch(diagnostic.kind.rule()) (#2336) 2023-01-29 21:15:09 -05:00
Simon Brugman
5165b703d9 Add VS Code to gitignore; fix typos (#2333) 2023-01-29 21:14:38 -05:00
Simon Brugman
b40cd1fabc debug assert for fix usage (#2335) 2023-01-29 21:13:42 -05:00
Charlie Marsh
64fb0bd2cc Include both ruff help and ruff help check in README (#2325) 2023-01-29 17:01:15 -05:00
Charlie Marsh
2ad29089af Allow list comprehensions for __all__ assignment (#2326) 2023-01-29 14:26:54 -05:00
Florian Best
f41796d559 feat: add ruff --statistics (#2284)
Closes #2284.
2023-01-29 13:44:56 -05:00
Samuel Cormier-Iijima
945a9e187c Migrate violations to named fields (#2317)
Fairly mechanical. Did a few of the simple cases manually to make sure things were working, and I think the rest will be easily achievable via a quick `fastmod` command.

ref #1871
2023-01-29 13:29:53 -05:00
Charlie Marsh
546413defb Fix remaining RelativeImportsOrder typo 2023-01-29 11:33:12 -05:00
Charlie Marsh
e371ef9b1a Place star before other member imports (#2320)
I think we've never run into this case, since it's rare to import `*` from a module _and_ import some other member explicitly. But we were deviating from `isort` by placing the `*` after other members, rather than up-top.

Closes #2318.
2023-01-28 22:17:43 -05:00
Charlie Marsh
c9585fe304 Run generate-all 2023-01-28 22:13:07 -05:00
Charlie Marsh
535868f939 Update fixable list (#2316) 2023-01-28 20:18:55 -05:00
Chirag
cec993aaa9 Add ruff . to documentation (#2307) 2023-01-28 14:53:11 -05:00
Samuel Cormier-Iijima
1a32d873f0 Fix regression with line-based rules not being ignored per-file (#2311) 2023-01-28 14:48:32 -05:00
Samuel Cormier-Iijima
f308f9f27e Respect per-file-ignores when checking noqa (#2309)
`RUF100` does not take into account a rule ignored for a file via a `per-file-ignores` configuration. To see this, try the following pyproject.toml:

```toml
[tool.ruff.per-file-ignores]
"test.py" = ["F401"]
```

and this test.py file:

```python
import itertools  # noqa: F401
```

Running `ruff --extend-select RUF100 test.py`, we should expect to get this error:

```
test.py:1:19: RUF100 Unused `noqa` directive (unused: `F401`)
```

The issue is that the per-file-ignores diagnostics are filtered out after the noqa checks, rather than before.
2023-01-28 14:16:30 -05:00
Jonathan Plasse
73dccce5f5 Isolate integration tests (#2306) 2023-01-28 13:32:50 -05:00
Charlie Marsh
fc9fae6579 Remove picture tag from PyPI and MkDocs 2023-01-28 11:49:52 -05:00
Charlie Marsh
add7fefeb5 Bump version to 0.0.237 2023-01-28 10:52:14 -05:00
Charlie Marsh
ec24947865 Fix version shorthand detection to use -V instead of -v (#2301)
Fixes a regression introduced in eda2be6350 (but not yet released to users). (`-v` is a real flag, but it's an alias for `--verbose`, not `--version`.)

Closes #2299.
2023-01-28 10:47:47 -05:00
Charlie Marsh
8038d32649 Deploy under docs subdirectory 2023-01-28 10:28:40 -05:00
Charlie Marsh
0362cc1098 Allow manual trigger for docs 2023-01-28 10:24:52 -05:00
Charlie Marsh
860e3110c0 Serve docs under /docs subdirectory 2023-01-28 10:22:35 -05:00
Jonxslays
0fa8c578cb Fix typo in typing_extensions (#2298) 2023-01-28 10:03:54 -05:00
Charlie Marsh
861df12269 Preserve global binding kind during reassignments (#2297) 2023-01-28 08:40:09 -05:00
Charlie Marsh
071e3fd196 Split MkDocs site into multiple pages (#2296) 2023-01-28 08:31:16 -05:00
Martin Fischer
dd79ec293a Rename new explain subcommand to rule
We probably want to introduce multiple explain subcommands and
overloading `explain` to explain it all seems like a bad idea.
We may want to introduce a subcommand to explain config options and
config options may end up having the same name as their rules, e.g. the
current `banned-api` is both a rule name (although not yet exposed to
the user) and a config option.

The idea is:

* `ruff rule` lists all rules supported by ruff
* `ruff rule <code>` explains a specific rule
* `ruff linter` lists all linters supported by ruff
* `ruff linter <name>` lists all rules/options supported by a specific linter

(After this commit only the 2nd case is implemented.)
2023-01-28 07:26:20 -05:00
Martin Fischer
5d331e43bf fix: help text and env for --format option of explain subcommand
The doc comment and the env attribute were copied by mistake.
2023-01-28 07:26:20 -05:00
Martin Fischer
ff3563b8ce Remove --check alias introduced in eda2be63
We do not want to support the --{subcommand} legacy format for new
subcommands ... only for subcommands that used this format previously.
2023-01-28 07:26:20 -05:00
Matt Morris
caada2f8bb add missing backticks to flake8 plugin urls in README (#2291) 2023-01-28 07:16:23 -05:00
messense
0b4cc5ac12 Add readme field to pyproject.toml (#2293) 2023-01-28 07:14:58 -05:00
Charlie Marsh
8c70247188 Switch to red MkDocs theme 2023-01-27 23:15:49 -05:00
Charlie Marsh
eaac3cae5e Add MkDocs version of README (#2287)
Co-authored-by: Justin Flannery <juftin@juftin.com>
2023-01-27 22:57:42 -05:00
Charlie Marsh
fd56414b2f Re-add ALL disclaimer 2023-01-27 22:18:20 -05:00
Martin Fischer
9731f96fb4 fix: typo in BREAKING_CHANGES.md & improve wrapping 2023-01-27 21:35:41 -05:00
Charlie Marsh
1a0191f1ac Add release to breaking changes 2023-01-27 20:34:24 -05:00
Charlie Marsh
249cf73d4e Tweak some wording in CLI help (#2285) 2023-01-27 20:25:58 -05:00
Martin Fischer
eda2be6350 Use subcommands for CLI instead of incompatible boolean flags
This commit greatly simplifies the implementation of the CLI,
as well as the user expierence (since --help no longer lists all
options even though many of them are in fact incompatible).

To preserve backwards-compatability as much as possible aliases have
been added for the new subcommands, so for example the following two
commands are equivalent:

    ruff explain E402 --format json
    ruff --explain E402 --format json

However for this to work the legacy-format double-dash command has to
come first, i.e. the following no longer works:

    ruff --format json --explain E402

Since ruff previously had an implicitly default subcommand,
this is preserved for backwards compatibility, i.e. the following two
commands are equivalent:

    ruff .
    ruff check .

Previously ruff didn't complain about several argument combinations that
should have never been allowed, e.g:

    ruff --explain RUF001 --line-length 33

previously worked but now rightfully fails since the explain command
doesn't support a `--line-length` option.
2023-01-27 19:38:17 -05:00
Charlie Marsh
57a68f7c7d Document the location of the personal config file (#2283) 2023-01-27 19:25:55 -05:00
Charlie Marsh
a19dd9237b Add comparison to type checkers (#2282) 2023-01-27 19:18:40 -05:00
Simon Brugman
4f067d806e add clippy and rust_dev to pre-commit (#2256)
I presume the reasoning for not including clippy in `pre-commit` was that it passes all files. This can be turned off with `pass_filenames`, in which case it only runs once.

`cargo +nightly dev generate-all` is also added (when excluding `target` is does not give false positives).

(The overhead of these commands is not much when the build is there. People can always choose to run only certain hooks with `pre-commit run [hook] --all-files`)
2023-01-27 18:53:44 -05:00
Samuel Cormier-Iijima
dd15c69181 [flake8-bandit] Add Rule S110 (try/except/pass) (#2197) 2023-01-27 18:52:55 -05:00
Charlie Marsh
b692921160 Use rustup show in lieu of actions-rs/toolchain (#2280) 2023-01-27 18:51:41 -05:00
Charlie Marsh
b3e8b1b787 Expand heuristic for detecting logging calls (#2279) 2023-01-27 18:41:16 -05:00
Charlie Marsh
0b34ca7107 Move off nightly Rust for dev workflows (#2278) 2023-01-27 18:35:19 -05:00
Charlie Marsh
df44c5124e Add missing autofix levels to sometimes-fixable rules 2023-01-27 18:25:23 -05:00
Simon Brugman
0e27f78b3f feat: include os.getcwdb (bytes) into flake8-use-pathlib (#2276) 2023-01-27 18:25:02 -05:00
Florian Best
cd8ad1df08 mark some fixers as sometimes-fixable (#2271) 2023-01-27 18:23:32 -05:00
Charlie Marsh
d1aaf16e40 Omit typing module from flake8-type-checking by default (#2277) 2023-01-27 18:19:45 -05:00
Ville Skyttä
7320058ce2 Incompatiblity warning updates (#2272) 2023-01-27 18:17:23 -05:00
Aarni Koskela
3a8b367b1c flake8-annotations: Move has_any_typed_arg into correct nested if (#2269) 2023-01-27 18:15:46 -05:00
Ville Skyttä
221b87332c feat: add more DTZ fix suggestions in messages (#2274) 2023-01-27 18:14:17 -05:00
Franck Nijhof
8149c8cbc4 Treat attribute constants as constant for yoda-conditions (#2266)
Accessed attributes that are Python constants should be considered for yoda-conditions


```py
## Error
JediOrder.YODA == age  # SIM300

## OK
age == JediOrder.YODA
```

~~PS: This PR will fail CI, as the `main` branch currently failing.~~
2023-01-27 12:55:12 -05:00
Charlie Marsh
2c415016a6 Update F401 snapshots 2023-01-27 12:43:42 -05:00
Sladyn
bb85119ba8 Convert UnusedImport violation to struct fields (#2141) 2023-01-27 11:31:39 -05:00
Simon Brugman
94551a203e feat: pylint PLE0604 and PLE0605 (#2241) 2023-01-27 11:26:33 -05:00
Charlie Marsh
64c4e4c6c7 Treat constant tuples as constants for yoda-conditions (#2265) 2023-01-27 11:25:57 -05:00
Charlie Marsh
84e4b7c96f Treat builtins as synthetically used (#2251) 2023-01-27 11:25:45 -05:00
Franck Nijhof
ca26f664ec Fix SIM300 to take Python constants into account (#2255)
SIM300 currently doesn't take Python constants into account when looking for Yoda conditions, this PR fixes that behavior.

```python
# Errors
YODA == age  # SIM300
YODA > age  # SIM300
YODA >= age  # SIM300

# OK
age == YODA
age < YODA
age <= YODA
```

Ref: <https://github.com/home-assistant/core/pull/86793>
2023-01-27 11:20:21 -05:00
Aarni Koskela
779b232db9 Fix typo: RelatveImportsOrder (#2264) 2023-01-27 11:15:43 -05:00
Charlie Marsh
a316b26b49 Rewrite some string-format violation messages (#2242) 2023-01-26 19:42:16 -05:00
Charlie Marsh
685d9ab848 Bump version to 0.0.236 2023-01-26 18:47:00 -05:00
Charlie Marsh
093f9156e1 Wrap return-bool-condition-directly fixes in bool() (#2240) 2023-01-26 18:22:34 -05:00
Charlie Marsh
615e62ae24 Clarify E-category rule support (#2239) 2023-01-26 18:12:28 -05:00
Charlie Marsh
76a0c45773 Track type-checking blocks during tree traversal (#2238) 2023-01-26 18:09:28 -05:00
Charlie Marsh
3ec46f0936 Allow pytest in shebang (#2237) 2023-01-26 17:32:23 -05:00
Charlie Marsh
a6ec2eb044 Avoid removing trailing comments on pass statements (#2235)
This isn't super consistent with some other rules, but... if you have a lone body, with a `pass`, followed by a comment, it's probably surprising if it gets removed. Let's retain the comment.

Closes #2231.
2023-01-26 17:29:13 -05:00
Simon Brugman
9d3a5530af refactor: move violations to linters (#2234) 2023-01-26 17:28:14 -05:00
Simon Brugman
8766e6a666 docs(readme): add featuretools (#2236) 2023-01-26 17:24:45 -05:00
Charlie Marsh
b08367b5a8 Avoid flagging blind exceptions with valid logging (#2232) 2023-01-26 17:05:01 -05:00
Charlie Marsh
98a8330124 Add stylist settings to all LibCST invocations (#2225) 2023-01-26 16:59:33 -05:00
Charlie Marsh
4d52ea87ef Implement exempt-modules setting from flake8-type-checking (#2230) 2023-01-26 16:55:32 -05:00
Charlie Marsh
291239b9f1 Fix range for try-consider-else (#2228) 2023-01-26 16:36:18 -05:00
Charlie Marsh
224334b6d1 Avoid erroneous class autofixes in indented blocks (#2226) 2023-01-26 16:24:21 -05:00
Charlie Marsh
0cab3f8437 Preserve indentation when fixing via LibCST (#2223) 2023-01-26 16:09:35 -05:00
Charlie Marsh
5f8810e987 Add strictness setting for flake8-typing-imports (#2221) 2023-01-26 16:04:21 -05:00
Charlie Marsh
f15c562a1c Remove unused overridden property (#2217) 2023-01-26 14:46:46 -05:00
Martin Fischer
4f3b63edd4 fix: --explain reporting the wrong linter
Fixes a regression introduced in 4e4643aa5d.

We want the longest prefixes to be checked first so we of course
have to reverse the sorting when sorting by prefix length.

Fixes #2210.
2023-01-26 13:53:35 -05:00
Simon Brugman
bab8691132 chore: fix script indent (#2213) 2023-01-26 13:53:22 -05:00
Charlie Marsh
50c85fd192 Add a fixable and unfixable example to the docs (#2211) 2023-01-26 13:23:21 -05:00
Martin Fischer
23819ae338 Group options in --help output and sort them by importance
`ruff --help` previously listed 37 options in no particular order
(with niche options like --isolated being listed before before essential
options such as --select).  This commit remedies that and additionally
groups the options by making use of the Clap help_heading feature.

Note that while the source code has previously also referred to
--add-noqa, --show-settings, and --show-files as "subcommands"
this commit intentionally does not list them under the new
Subcommands section since contrary to --explain and --clean
combining them with most of the other options makes sense.
2023-01-26 13:06:29 -05:00
Martin Fischer
4bf2879067 refactor: Move add_noqa if branch up 2023-01-26 13:06:29 -05:00
Martin Fischer
b359f3a9ff refactor: Get rid of ruff::resolver::FileDiscovery 2023-01-26 13:06:29 -05:00
Martin Fischer
73d0461d55 refactor: Check required_version in Settings::from_configuration
The idiomatic way in Rust is to make invalid types unrepresentable
instead of paranoidly calling a validate method everywhere.
2023-01-26 13:06:29 -05:00
Martin Fischer
55bb36fb8b refactor: Introduce LogLevelArgs 2023-01-26 13:06:29 -05:00
Martin Fischer
cebea16fe4 refactor: Move Args::partition call after panic::set_hook 2023-01-26 13:06:29 -05:00
Charlie Marsh
f7be192f8b Alphabetize Flake8 plugins in the README (#2209) 2023-01-26 13:05:30 -05:00
Edgar R. M
e88275280b Implement some rules from flake8-logging-format (#2150) 2023-01-26 12:58:10 -05:00
Samuel Cormier-Iijima
7370a27c09 Don't flag B009/B010 if identifiers would be mangled (#2204) 2023-01-26 12:56:18 -05:00
jvstme
0ad6b8224d Fix typo in src option docs (#2201) 2023-01-26 12:23:09 -05:00
Henry Schreiner
f3aa409d9a docs(readme): add pypa's build (#2200) 2023-01-26 12:18:04 -05:00
Charlie Marsh
adb5c5b150 Fix respect_gitignore reference (#2196) 2023-01-26 09:53:17 -05:00
Martin Fischer
b69b6a7ec8 refactor: Move comments before if conditions 2023-01-25 22:08:35 -05:00
Martin Fischer
a7d2def9cd refactor: Move ruff_cli::resolve to own module 2023-01-25 22:08:35 -05:00
Martin Fischer
d9ead4e6df refactor: Rename CLI arg structs from Cli to Args
Technically the command-line interface (CLI) encompasses both input and
output, so naming the input structs 'Args' is more accurate than 'Cli'.
2023-01-25 22:08:35 -05:00
Charlie Marsh
b346f74915 Run cargo update (#2185) 2023-01-25 21:32:44 -05:00
Eric Roberts
708295f4c9 Move violations for pycodestyle rules to rules files (#2138) 2023-01-25 20:11:36 -05:00
Charlie Marsh
4190f1045e Remove manual newline from autofix helpers (#2184) 2023-01-25 19:53:26 -05:00
Florian Stasse
353857e2a5 Implement TRY400 (#2115) 2023-01-25 19:42:19 -05:00
Denis Gavrilyuk
55b43c8ea7 feat: implement TRY002 and TRY003 (#2135) 2023-01-25 19:22:43 -05:00
Anders Kaseorg
6036d1bbe2 flake8_executable: Only match shebang at beginning of line (#2183)
The Python implementation uses `re.match` for this, which only matches
at the beginning of a line.

https://github.com/xuhdev/flake8-executable/blob/v2.1.3/flake8_executable/__init__.py#L124

We could use `Regex::captures_read_at`, but that’s a more complicated
API; it’s easier to anchor the regex with `^`.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-01-25 18:57:09 -05:00
569 changed files with 12086 additions and 6946 deletions

19
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
changelog:
categories:
- title: Breaking Changes
labels:
- breaking
- title: Rules
labels:
- rule
- autofix
- title: Settings
labels:
- configuration
- title: Bug Fixes
labels:
- bug
- title: Other Changes
labels:
- "*"

View File

@@ -22,16 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v1
- run: cargo build --all
- run: ./target/debug/ruff_dev generate-all
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. Run 'cargo +nightly dev generate-all'."
- run: git diff --quiet ruff.schema.json || echo "::error file=ruff.schema.json::This file is outdated. Run 'cargo +nightly dev generate-all'."
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. Run 'cargo dev generate-all'."
- run: git diff --quiet ruff.schema.json || echo "::error file=ruff.schema.json::This file is outdated. Run 'cargo dev generate-all'."
- run: git diff --exit-code -- README.md ruff.schema.json
cargo-fmt:
@@ -39,12 +36,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
components: rustfmt
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- run: cargo fmt --all --check
cargo_clippy:
@@ -52,13 +45,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
components: clippy
target: wasm32-unknown-unknown
- name: "Install Rust toolchain"
run: |
rustup component add clippy
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v1
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic
- run: cargo clippy -p ruff --target wasm32-unknown-unknown --all-features -- -D warnings -W clippy::pedantic
@@ -71,20 +61,17 @@ jobs:
name: "cargo test | ${{ matrix.os }}"
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
override: true
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v1
- run: cargo install cargo-insta
- run: pip install black[d]==22.12.0
- name: Run tests (Ubuntu)
- name: "Run tests (Ubuntu)"
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
cargo insta test --all --delete-unreferenced-snapshots
git diff --exit-code
- name: Run tests (Windows)
- name: "Run tests (Windows)"
if: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: |
@@ -102,10 +89,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v1
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
- run: cargo check
@@ -114,61 +99,26 @@ jobs:
./scripts/add_rule.py --name FirstRule --code TST001 --linter test
- run: cargo check
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425
# wasm-pack-test:
# name: "wasm-pack test"
# runs-on: ubuntu-latest
# env:
# WASM_BINDGEN_TEST_TIMEOUT: 60
# steps:
# - uses: actions/checkout@v3
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: nightly-2022-11-01
# override: true
# - uses: actions/cache@v3
# env:
# cache-name: cache-cargo
# with:
# path: |
# ~/.cargo/registry
# ~/.cargo/git
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - uses: jetli/wasm-pack-action@v0.4.0
# - uses: jetli/wasm-bindgen-action@v0.2.0
# - run: wasm-pack test --node
maturin-build:
name: "maturin build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
override: true
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v1
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install maturin
- run: maturin build -b bin
- run: python scripts/transform_readme.py --target pypi
typos:
name: Spell Check with Typos
name: "spell check"
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v3
- name: Check spelling of file.txt
uses: crate-ci/typos@master
- uses: actions/checkout@v3
- uses: crate-ci/typos@master
with:
files: .

35
.github/workflows/docs.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: mkdocs
on:
push:
paths:
- README.md
- mkdocs.template.yml
- .github/workflows/docs.yaml
branches: [ main ]
workflow_dispatch:
jobs:
mkdocs:
runs-on: ubuntu-latest
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: "Install dependencies"
run: |
pip install "mkdocs~=1.4.2" "mkdocs-material~=9.0.6"
- name: "Copy README File"
run: |
python scripts/transform_readme.py --target mkdocs
python scripts/generate_mkdocs.py
mkdocs build --strict
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@2.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages publish site --project-name=ruff-docs --branch ${GITHUB_HEAD_REF} --commit-hash ${GITHUB_SHA}

View File

@@ -24,21 +24,17 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels - x86_64
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - x86_64"
uses: messense/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel - x86_64
- name: "Install built wheel - x86_64"
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -51,20 +47,16 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels - universal2
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - universal2"
uses: messense/maturin-action@v1
with:
args: --release --universal2 --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel - universal2
- name: "Install built wheel - universal2"
run: |
pip install dist/${{ env.CRATE_NAME }}-*universal2.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -81,22 +73,18 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel
- name: "Install built wheel"
shell: bash
run: |
python -m pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -113,17 +101,17 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -139,7 +127,7 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
@@ -158,7 +146,7 @@ jobs:
pip3 install -U pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -177,13 +165,13 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
with:
@@ -192,7 +180,7 @@ jobs:
run: |
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -212,7 +200,7 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
@@ -228,7 +216,7 @@ jobs:
apk add py3-pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -252,17 +240,17 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: pypy${{ matrix.python-version }}
- name: Build wheels
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i pypy${{ matrix.python-version }} -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -285,7 +273,7 @@ jobs:
with:
name: wheels
- uses: actions/setup-python@v4
- name: Publish to PyPi
- name: "Publish to PyPi"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.FLAKE8_TO_RUFF_TOKEN }}

View File

@@ -18,12 +18,8 @@ jobs:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
target: wasm32-unknown-unknown
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v3
with:
node-version: 18

View File

@@ -26,21 +26,17 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels - x86_64
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: messense/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist
- name: Install built wheel - x86_64
- name: "Install built wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -53,20 +49,16 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels - universal2
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - universal2"
uses: messense/maturin-action@v1
with:
args: --release --universal2 --out dist
- name: Install built wheel - universal2
- name: "Install built wheel - universal2"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -83,22 +75,18 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
default: true
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
- name: Install built wheel
- name: "Install built wheel"
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -115,17 +103,19 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -141,7 +131,9 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
@@ -160,7 +152,7 @@ jobs:
pip3 install -U pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -179,13 +171,15 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
with:
@@ -194,7 +188,7 @@ jobs:
run: |
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -214,7 +208,9 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
@@ -230,7 +226,7 @@ jobs:
apk add py3-pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -254,17 +250,19 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: pypy${{ matrix.python-version }}
- name: Build wheels
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i pypy${{ matrix.python-version }}
- name: Install built wheel
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: Upload wheels
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
@@ -288,13 +286,13 @@ jobs:
with:
name: wheels
- uses: actions/setup-python@v4
- name: Publish to PyPi
- name: "Publish to PyPi"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.RUFF_TOKEN }}
run: |
pip install --upgrade twine
twine upload --skip-existing *
- name: Update pre-commit mirror
- name: "Update pre-commit mirror"
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "pypi_release"}'

5
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# Local cache
.ruff_cache
resources/test/cpython
docs/
mkdocs.yml
###
# Rust.gitignore
@@ -182,3 +184,6 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vimspector.json
# Visual Studio Code
.vscode/

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.235
rev: v0.0.238
hooks:
- id: ruff
args: [--fix]
@@ -15,6 +15,17 @@ repos:
hooks:
- id: cargo-fmt
name: cargo fmt
entry: cargo +nightly fmt --
entry: cargo fmt --
language: rust
types: [rust]
- id: clippy
name: clippy
entry: cargo clippy --workspace --all-targets --all-features
language: rust
pass_filenames: false
- id: dev-generate-all
name: dev-generate-all
entry: cargo dev generate-all
language: rust
pass_filenames: false
exclude: target

View File

@@ -1,5 +1,81 @@
# Breaking Changes
## 0.0.238
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/charliermarsh/ruff/pull/2312))
Previously, the interplay between `select` and its related options could lead to unexpected
behavior. For example, `ruff --select E501 --ignore ALL` and `ruff --select E501 --extend-ignore
ALL` behaved differently. (See [#2312](https://github.com/charliermarsh/ruff/pull/2312) for more
examples.)
When Ruff determines the enabled rule set, it has to reconcile `select` and `ignore` from a variety
of sources, including the current `pyproject.toml`, any inherited `pyproject.toml` files, and the
CLI.
The new semantics are such that Ruff uses the "highest-priority" `select` as the basis for the rule
set, and then applies any `extend-select`, `ignore`, and `extend-ignore` adjustments. CLI options
are given higher priority than `pyproject.toml` options, and the current `pyproject.toml` file is
given higher priority than any inherited `pyproject.toml` files.
`extend-select` and `extend-ignore` are no longer given "top priority"; instead, they merely append
to the `select` and `ignore` lists, as in Flake8.
This change is largely backwards compatible -- most users should experience no change in behavior.
However, as an example of a breaking change, consider the following:
```toml
[tool.ruff]
ignore = ["F401"]
```
Running `ruff --select F` would previously have enabled all `F` rules, apart from `F401`. Now, it
will enable all `F` rules, including `F401`, as the command line's `--select` resets the resolution.
### `remove-six-compat` (`UP016`) has been removed ([#2332](https://github.com/charliermarsh/ruff/pull/2332))
The `remove-six-compat` rule has been removed. This rule was only useful for one-time Python 2-to-3
upgrades.
## 0.0.238
### `--explain`, `--clean`, and `--generate-shell-completion` are now subcommands ([#2190](https://github.com/charliermarsh/ruff/pull/2190))
`--explain`, `--clean`, and `--generate-shell-completion` are now implemented as subcommands:
ruff . # Still works! And will always work.
ruff check . # New! Also works.
ruff --explain E402 # Still works.
ruff rule E402 # New! Also works. (And preferred.)
# Oops! The command has to come first.
ruff --format json --explain E402 # No longer works.
ruff --explain E402 --format json # Still works!
ruff rule E402 --format json # Works! (And preferred.)
This change is largely backwards compatible -- most users should experience
no change in behavior. However, please note the following exceptions:
* Subcommands will now fail when invoked with unsupported arguments, instead
of silently ignoring them. For example, the following will now fail:
ruff --clean --respect-gitignore
(the `clean` command doesn't support `--respect-gitignore`.)
* The semantics of `ruff <arg>` have changed slightly when `<arg>` is a valid subcommand.
For example, prior to this release, running `ruff rule` would run `ruff` over a file or
directory called `rule`. Now, `ruff rule` would invoke the `rule` subcommand. This should
only impact projects with files or directories named `rule`, `check`, `explain`, `clean`,
or `generate-shell-completion`.
* Scripts that invoke ruff should supply `--` before any positional arguments.
(The semantics of `ruff -- <arg>` have not changed.)
* `--explain` previously treated `--format grouped` as a synonym for `--format text`.
This is no longer supported; instead, use `--format text`.
## 0.0.226
### `misplaced-comparison-constant` (`PLC2201`) was deprecated in favor of `SIM300` ([#1980](https://github.com/charliermarsh/ruff/pull/1980))

View File

@@ -39,20 +39,24 @@ 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.
For rustfmt and Clippy, we use [nightly Rust][nightly], as it is stricter than stable Rust.
(However, tests and builds use stable Rust.)
and that it passes both the lint and test validation checks:
```shell
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --fix --workspace --all-targets --all-features # Linting...
cargo fmt --all # Auto-formatting...
cargo clippy --fix --workspace --all-targets --all-features # Linting...
cargo test --all # 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.
If you have `pre-commit` [installed](https://pre-commit.com/#installation) then you can use it to
assist with formatting and linting. The following command will run the `pre-commit` hooks:
```shell
pre-commit run --all-files
```
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
@@ -79,7 +83,7 @@ To trigger the violation, you'll likely want to augment the logic in `src/checke
defines the Python AST visitor, responsible for iterating over the abstract syntax tree and
collecting diagnostics as it goes.
If you need to inspect the AST, you can run `cargo +nightly dev print-ast` with a Python file. Grep
If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. 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/[linter]`, named to match
@@ -87,7 +91,7 @@ the code you defined earlier (e.g., `resources/test/fixtures/pycodestyle/E402.py
contain a variety of violations and non-violations designed to evaluate and demonstrate the behavior
of your lint rule.
Run `cargo +nightly dev generate-all` to generate the code for your new fixture. Then run Ruff
Run `cargo dev generate-all` to generate the code for your new fixture. Then run Ruff
locally with (e.g.) `cargo run resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
@@ -95,7 +99,7 @@ Once you're satisfied with the output, codify the behavior as a snapshot test by
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.
Finally, regenerate the documentation and generated code with `cargo +nightly dev generate-all`.
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
### Example: Adding a new configuration option
@@ -121,7 +125,7 @@ You may also want to add the new configuration option to the `flake8-to-ruff` to
responsible for converting `flake8` configuration files to Ruff's TOML format. This logic
lives in `flake8_to_ruff/src/converter.rs`.
Finally, regenerate the documentation and generated code with `cargo +nightly dev generate-all`.
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
## Release process
@@ -131,5 +135,3 @@ 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).
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust

253
Cargo.lock generated
View File

@@ -82,9 +82,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.7"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3"
checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e"
dependencies = [
"bstr 1.1.0",
"doc-comment",
@@ -181,9 +181,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.11.1"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "cachedir"
@@ -236,12 +236,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "ciborium"
version = "0.2.0"
@@ -283,13 +277,13 @@ dependencies = [
[[package]]
name = "clap"
version = "4.0.32"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex 0.3.0",
"clap_lex 0.3.1",
"is-terminal",
"once_cell",
"strsim",
@@ -298,11 +292,11 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.0.7"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b"
checksum = "3d6540eedc41f8a5a76cf3d8d458057dcdf817be4158a55b5f861f7a5483de75"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
]
[[package]]
@@ -311,26 +305,26 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4160b4a4f72ef58bd766bad27c09e6ef1cc9d82a22f6a0f55d152985a4a48e31"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
"clap_complete",
"clap_complete_fig",
]
[[package]]
name = "clap_complete_fig"
version = "4.0.2"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3"
checksum = "cf0c76d8fcf782a1102ccfcd10ca8246e7fdd609c1cd6deddbb96cb638e9bb5c"
dependencies = [
"clap 4.0.32",
"clap 4.1.4",
"clap_complete",
]
[[package]]
name = "clap_derive"
version = "4.0.21"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
dependencies = [
"heck",
"proc-macro-error",
@@ -350,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [
"os_str_bytes",
]
@@ -399,15 +393,14 @@ checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
[[package]]
name = "console"
version = "0.15.2"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c"
checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"terminal_size",
"winapi",
"windows-sys",
]
[[package]]
@@ -551,9 +544,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -563,9 +556,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200"
dependencies = [
"cc",
"codespan-reporting",
@@ -578,15 +571,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea"
[[package]]
name = "cxxbridge-macro"
version = "1.0.85"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e"
dependencies = [
"proc-macro2",
"quote",
@@ -757,10 +750,10 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.235"
version = "0.0.238"
dependencies = [
"anyhow",
"clap 4.0.32",
"clap 4.1.4",
"colored",
"configparser",
"once_cell",
@@ -771,7 +764,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml 0.6.0",
"toml",
]
[[package]]
@@ -833,18 +826,18 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [
"aho-corasick",
"bstr 0.2.17",
"bstr 1.1.0",
"fnv",
"log",
"regex",
@@ -931,11 +924,10 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.18"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
@@ -989,9 +981,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.23.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48b08a091dfe5b09a6a9688c468fdd5b4396e92ce09e2eb932f0884b02788a4"
checksum = "f6f0f08b46e4379744de2ab67aa8f7de3ffd1da3e275adc41fcc82053ede46ff"
dependencies = [
"console",
"lazy_static",
@@ -1014,9 +1006,9 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
dependencies = [
"libc",
"windows-sys",
@@ -1242,9 +1234,9 @@ dependencies = [
[[package]]
name = "matches"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
@@ -1302,9 +1294,9 @@ checksum = "d906846a98739ed9d73d66e62c2641eef8321f1734b7a1156ab045a0248fb2b3"
[[package]]
name = "nix"
version = "0.26.1"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
@@ -1369,9 +1361,9 @@ dependencies = [
[[package]]
name = "num-complex"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
dependencies = [
"num-traits",
"serde",
@@ -1408,18 +1400,18 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.5.7"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
checksum = "8d829733185c1ca374f17e52b762f24f535ec625d2cc1f070e34c8a9068f341b"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.7"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
checksum = "2be1598bf1c313dcdd12092e3f1920f463462525a21b7b4e11b4168353d0123e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -1457,9 +1449,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.5"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
dependencies = [
"cfg-if",
"libc",
@@ -1527,9 +1519,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pest"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a"
checksum = "4ab62d2fa33726dbe6321cc97ef96d8cde531e3eeaf858a058de53a8a6d40d8f"
dependencies = [
"thiserror",
"ucd-trie",
@@ -1537,9 +1529,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6"
checksum = "8bf026e2d0581559db66d837fe5242320f525d85c76283c61f4d51a1238d65ea"
dependencies = [
"pest",
"pest_generator",
@@ -1547,9 +1539,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c"
checksum = "2b27bd18aa01d91c8ed2b61ea23406a676b42d82609c6e2581fba42f0c15f17f"
dependencies = [
"pest",
"pest_meta",
@@ -1560,9 +1552,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.5.3"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51"
checksum = "9f02b677c1859756359fc9983c2e56a0237f18624a3789528804406b7e915e5d"
dependencies = [
"once_cell",
"pest",
@@ -1703,9 +1695,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.4"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54fc5dc63ed3bbf19494623db4f3af16842c0d975818e469022d09e53f0aa05"
checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [
"difflib",
"itertools",
@@ -1730,13 +1722,12 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "1.2.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34"
dependencies = [
"once_cell",
"thiserror",
"toml 0.5.11",
"toml_edit",
]
[[package]]
@@ -1765,9 +1756,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
dependencies = [
"unicode-ident",
]
@@ -1852,9 +1843,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.10.1"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3"
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
@@ -1884,9 +1875,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
@@ -1931,13 +1922,13 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.235"
version = "0.0.238"
dependencies = [
"anyhow",
"bitflags",
"cfg-if",
"chrono",
"clap 4.0.32",
"clap 4.1.4",
"colored",
"console_error_panic_hook",
"console_log",
@@ -1979,14 +1970,14 @@ dependencies = [
"textwrap",
"thiserror",
"titlecase",
"toml 0.6.0",
"toml",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "ruff_cli"
version = "0.0.235"
version = "0.0.238"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1995,7 +1986,7 @@ dependencies = [
"bincode",
"cachedir",
"chrono",
"clap 4.0.32",
"clap 4.1.4",
"clap_complete_command",
"clearscreen",
"colored",
@@ -2023,10 +2014,10 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.235"
version = "0.0.238"
dependencies = [
"anyhow",
"clap 4.0.32",
"clap 4.1.4",
"itertools",
"libcst",
"once_cell",
@@ -2044,7 +2035,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.235"
version = "0.0.238"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2071,9 +2062,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.36.5"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags",
"errno",
@@ -2085,9 +2076,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.20.7"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
@@ -2253,9 +2244,9 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]]
name = "serde"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
@@ -2273,9 +2264,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.151"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@@ -2448,23 +2439,13 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.1.3"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminfo"
version = "0.7.5"
@@ -2602,15 +2583,6 @@ dependencies = [
"regex",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.6.0"
@@ -2733,9 +2705,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.8"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58"
[[package]]
name = "unicode-ident"
@@ -2801,12 +2773,11 @@ dependencies = [
[[package]]
name = "ureq"
version = "2.5.0"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64",
"chunked_transfer",
"flate2",
"log",
"once_cell",
@@ -3000,9 +2971,9 @@ dependencies = [
[[package]]
name = "which"
version = "4.3.0"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
dependencies = [
"either",
"libc",
@@ -3063,45 +3034,45 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "yaml-rust"

View File

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

205
LICENSE
View File

@@ -800,3 +800,208 @@ are:
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- flake8-logging-format, licensed as follows:
"""
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

779
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ use ruff::flake8_to_ruff::{self, ExternalConfig};
about = "Convert existing Flake8 configuration to Ruff.",
long_about = None
)]
struct Cli {
struct Args {
/// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or
/// `.flake8`).
#[arg(required = true)]
@@ -41,15 +41,15 @@ struct Cli {
}
fn main() -> Result<()> {
let cli = Cli::parse();
let args = Args::parse();
// Read the INI file.
let mut ini = Ini::new_cs();
ini.set_multiline(true);
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
let config = ini.load(args.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let pyproject = cli.pyproject.map(flake8_to_ruff::parse).transpose()?;
let pyproject = args.pyproject.map(flake8_to_ruff::parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
@@ -60,7 +60,7 @@ fn main() -> Result<()> {
.unwrap_or_default();
// Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, &external_config, cli.plugin)?;
let pyproject = flake8_to_ruff::convert(&config, &external_config, args.plugin)?;
println!("{}", toml::to_string_pretty(&pyproject)?);
Ok(())

49
mkdocs.template.yml Normal file
View File

@@ -0,0 +1,49 @@
site_name: Ruff
theme:
name: material
features:
- navigation.instant
- navigation.tracking
- content.code.annotate
- toc.integrate
- toc.follow
- navigation.path
- navigation.top
- content.code.copy
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: red
toggle:
icon: material/weather-sunny
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: red
toggle:
icon: material/weather-night
name: Switch to light mode
repo_url: https://github.com/charliermarsh/ruff
repo_name: ruff
site_author: charliermarsh
site_url: https://beta.ruff.rs/docs/
site_dir: site/docs
markdown_extensions:
- toc:
permalink: "#"
- pymdownx.snippets:
- pymdownx.magiclink:
- attr_list:
- md_in_html:
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite:
- pymdownx.superfences:
- markdown.extensions.attr_list:
- pymdownx.keys:
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.highlight:
anchor_linenums: true
plugins:
- search

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.235"
version = "0.0.238"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
@@ -15,6 +15,7 @@ authors = [
maintainers = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
]
readme = "README.md"
requires-python = ">=3.7"
license = { file = "LICENSE" }
keywords = ["automation", "flake8", "pycodestyle", "pyflakes", "pylint", "clippy"]

View File

@@ -39,3 +39,10 @@ class Foo:
# Error
def __init__(self, foo: int):
...
# Error used to be ok for a moment since the mere presence
# of a vararg falsely indicated that the function has a typed argument.
class Foo:
def __init__(self, *arg):
...

View File

@@ -0,0 +1,14 @@
try:
pass
except Exception:
pass
try:
pass
except:
pass
try:
pass
except ValueError:
pass

View File

@@ -61,3 +61,34 @@ except Exception as e:
raise bad
except BaseException:
pass
import logging
try:
pass
except Exception:
logging.error("...")
try:
pass
except Exception:
logging.error("...", exc_info=False)
try:
pass
except Exception:
logging.error("...", exc_info=None)
try:
pass
except Exception:
logging.exception("...")
try:
pass
except Exception:
logging.error("...", exc_info=True)

View File

@@ -1,7 +1,7 @@
"""
Should emit:
B009 - Line 18, 19, 20, 21, 22
B010 - Line 33, 34, 35, 36
B009 - Line 19, 20, 21, 22, 23, 24
B010 - Line 40, 41, 42, 43, 44, 45
"""
# Valid getattr usage
@@ -13,10 +13,12 @@ getattr(foo, bar, None)
getattr(foo, "123abc")
getattr(foo, r"123\abc")
getattr(foo, "except")
getattr(foo, "__123abc")
# Invalid usage
getattr(foo, "bar")
getattr(foo, "_123abc")
getattr(foo, "__123abc__")
getattr(foo, "abc123")
getattr(foo, r"abc123")
_ = lambda x: getattr(x, "bar")
@@ -27,6 +29,7 @@ if getattr(x, "bar"):
setattr(foo, bar, None)
setattr(foo, "bar{foo}".format(foo="a"), None)
setattr(foo, "123abc", None)
setattr(foo, "__123abc", None)
setattr(foo, r"123\abc", None)
setattr(foo, "except", None)
_ = lambda x: setattr(x, "bar", 1)
@@ -36,6 +39,7 @@ if setattr(x, "bar", 1):
# Invalid usage
setattr(foo, "bar", None)
setattr(foo, "_123abc", None)
setattr(foo, "__123abc__", None)
setattr(foo, "abc123", None)
setattr(foo, r"abc123", None)
setattr(foo.bar, r"baz", None)

View File

@@ -7,6 +7,13 @@ import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tensorflow # unconventional
import holoviews # unconventional
import panel # unconventional
import plotly.express # unconventional
import matplotlib # unconventional
import polars # unconventional
import pyarrow # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
@@ -15,6 +22,13 @@ import dask.dataframe as ddf # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tensorflow as tfz # unconventional
import holoviews as hsv # unconventional
import panel as pns # unconventional
import plotly.express as pltx # unconventional
import matplotlib as ml # unconventional
import polars as ps # unconventional
import pyarrow as arr # unconventional
import altair as alt # conventional
import dask.array as da # conventional
@@ -23,3 +37,12 @@ import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tensorflow as tf # conventional
import holoviews as hv # conventional
import panel as pn # conventional
import plotly.express as px # conventional
import matplotlib as mpl # conventional
import polars as pl # conventional
import pyarrow as pa # conventional
from tensorflow.keras import Model # conventional

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello {}".format("World!"))

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello %s" % "World!")

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello" + " " + "World!")

View File

@@ -0,0 +1,4 @@
import logging
name = "world"
logging.info(f"Hello {name}")

View File

@@ -0,0 +1,3 @@
import logging
logging.warn("Hello World!")

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello world!",
extra={
"name": "foobar",
},
)

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello world!",
extra=dict(
name="foobar",
),
)

View File

@@ -0,0 +1,21 @@
import logging
import sys
# G201
try:
pass
except:
logging.error("Hello World", exc_info=True)
try:
pass
except:
logging.error("Hello World", exc_info=sys.exc_info())
# OK
try:
pass
except:
logging.error("Hello World", exc_info=False)
logging.error("Hello World", exc_info=sys.exc_info())

View File

@@ -0,0 +1,21 @@
import logging
import sys
# G202
try:
pass
except:
logging.exception("Hello World", exc_info=True)
try:
pass
except:
logging.exception("Hello World", exc_info=sys.exc_info())
# OK
try:
pass
except:
logging.exception("Hello World", exc_info=False)
logging.exception("Hello World", exc_info=True)

View File

@@ -0,0 +1,7 @@
import argparse
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("target_dir", type=Path)
args = parser.parse_args()
parser.error(f"Target directory {args.target_dir} does not exist")

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello {world}!",
extra=dict(
world="World",
),
)

View File

@@ -0,0 +1,8 @@
import logging
logging.info(
"Hello {world}!",
extra=dict(
world="{}".format("World"),
),
)

View File

@@ -0,0 +1,3 @@
import logging
logging.info("Hello World!")

View File

@@ -0,0 +1,3 @@
import warnings
warnings.warn("Hello World!")

View File

@@ -1,12 +1,12 @@
class Foo:
"""buzz"""
pass # PIE790
pass
if foo:
"""foo"""
pass # PIE790
pass
def multi_statement() -> None:
@@ -18,28 +18,28 @@ if foo:
pass
else:
"""bar"""
pass # PIE790
pass
while True:
pass
else:
"""bar"""
pass # PIE790
pass
for _ in range(10):
pass
else:
"""bar"""
pass # PIE790
pass
async for _ in range(10):
pass
else:
"""bar"""
pass # PIE790
pass
def foo() -> None:
@@ -47,7 +47,7 @@ def foo() -> None:
buzz
"""
pass # PIE790
pass
async def foo():
@@ -55,14 +55,14 @@ async def foo():
buzz
"""
pass # PIE790
pass
try:
"""
buzz
"""
pass # PIE790
pass
except ValueError:
pass
@@ -71,29 +71,34 @@ try:
bar()
except ValueError:
"""bar"""
pass # PIE790
pass
for _ in range(10):
"""buzz"""
pass # PIE790
pass
async for _ in range(10):
"""buzz"""
pass # PIE790
pass
while cond:
"""buzz"""
pass # PIE790
pass
with bar:
"""buzz"""
pass # PIE790
pass
async with bar:
"""buzz"""
pass # PIE790
pass
def foo() -> None:
"""buzz"""
pass # bar
class Foo:

View File

@@ -14,3 +14,60 @@ def ok_other_scope():
@pytest.fixture(scope="function")
def error():
...
@pytest.fixture(scope="function", name="my_fixture")
def error_multiple_args():
...
@pytest.fixture(name="my_fixture", scope="function")
def error_multiple_args():
...
@pytest.fixture(name="my_fixture", scope="function", **kwargs)
def error_second_arg():
...
# pytest.fixture does not take positional arguments, however this
# tests the general case as we use a helper function that should
# work for all cases.
@pytest.fixture("my_fixture", scope="function")
def error_arg():
...
@pytest.fixture(
scope="function",
name="my_fixture",
)
def error_multiple_args():
...
@pytest.fixture(
name="my_fixture",
scope="function",
)
def error_multiple_args():
...
@pytest.fixture(
"hello",
name,
*args
,
# another comment ,)
scope=\
"function" # some comment ),
,
name2=name, name3="my_fixture", **kwargs
)
def error_multiple_args():
...

View File

@@ -91,3 +91,10 @@ if True:
b = cccccccccccccccccccccccccccccccccccc
else:
b = ddddddddddddddddddddddddddddddddddddd
# OK (trailing comments)
if True:
exitcode = 0
else:
exitcode = 1 # Trailing comment

View File

@@ -1,15 +1,26 @@
# Errors
"yoda" == compare # SIM300
'yoda' == compare # SIM300
"yoda" == compare # SIM300
42 == age # SIM300
("a", "b") == compare # SIM300
"yoda" <= compare # SIM300
'yoda' < compare # SIM300
"yoda" < compare # SIM300
42 > age # SIM300
YODA == age # SIM300
YODA > age # SIM300
YODA >= age # SIM300
JediOrder.YODA == age # SIM300
# OK
compare == "yoda"
age == 42
compare == ("a", "b")
x == y
"yoda" == compare == 1
"yoda" == compare == someothervar
"yoda" == "yoda"
age == YODA
age < YODA
age <= YODA
YODA == YODA
age == JediOrder.YODA

View File

@@ -0,0 +1,16 @@
def f():
import pandas as pd
x: pd.DataFrame
def f():
import pandas.core.frame as pd
x: pd.DataFrame
def f():
import flask
x: flask

View File

@@ -0,0 +1,54 @@
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime,
# and implicitly imports `pkg.bar`.
import pkg
import pkg.bar
def test(value: pkg.bar.A):
return pkg.B()
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg.bar` is used at
# runtime, and implicitly imports `pkg`.
import pkg
import pkg.bar
def test(value: pkg.A):
return pkg.bar.B()
def f():
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
import pkg
from pkg import A
def test(value: A):
return pkg.B()
def f():
# In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime.
from pkg import A, B
def test(value: A):
return B()
def f():
# Even in strict mode, this shouldn't rase an error, since `pkg.baz` is used at
# runtime, and implicitly imports `pkg.bar`.
import pkg.bar
import pkg.baz
def test(value: pkg.bar.A):
return pkg.baz.B()
def f():
# In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime.
import pkg
from pkg.bar import A
def test(value: A):
return pkg.B()

View File

@@ -29,3 +29,4 @@ os.path.splitext(p)
with open(p) as fp:
fp.read()
open(p).close()
os.getcwdb(p)

View File

@@ -0,0 +1,29 @@
from numpy import (
cos,
int8,
int16,
int32,
int64,
sin,
tan,
uint8,
uint16,
uint32,
uint64,
)
if True:
# inside nested block
from numpy import (
cos,
int8,
int16,
int32,
int64,
sin,
tan,
uint8,
uint16,
uint32,
uint64,
)

View File

@@ -0,0 +1,3 @@
from .logging import config_logging
from .settings import ENV
from .settings import *

View File

@@ -0,0 +1,20 @@
import pandas as pd
x = pd.DataFrame()
x.drop(["a"], axis=1, inplace=True)
x.drop(["a"], axis=1, inplace=True)
x.drop(
inplace=True,
columns=["a"],
axis=1,
)
if True:
x.drop(
inplace=True,
columns=["a"],
axis=1,
)

View File

@@ -6,7 +6,7 @@ from typing import NewType
GLOBAL: str = "foo"
def f():
def assign():
global GLOBAL
GLOBAL = "bar"
lower = 0
@@ -18,4 +18,18 @@ def f():
MyObj2 = namedtuple("MyObj12", ["a", "b"])
T = TypeVar("T")
UserId = NewType('UserId', int)
UserId = NewType("UserId", int)
def aug_assign(rank, world_size):
global CURRENT_PORT
CURRENT_PORT += 1
if CURRENT_PORT > MAX_PORT:
CURRENT_PORT = START_PORT
def loop_assign():
global CURRENT_PORT
for CURRENT_PORT in range(5):
pass

View File

@@ -0,0 +1,6 @@
"""Test: explicit re-export of shadowed builtins."""
from concurrent.futures import (
CancelledError as CancelledError,
TimeoutError as TimeoutError,
)

View File

@@ -2,31 +2,37 @@
# Errors.
###
def f():
global x
global X
def f():
global x
global X
print(x)
print(X)
###
# Non-errors.
###
def f():
global x
global X
x = 1
X = 1
def f():
global x
global X
(x, y) = (1, 2)
(X, y) = (1, 2)
def f():
global x
global X
del x
del X
def f():
global X
X += 1

View File

@@ -0,0 +1,19 @@
__all__ = "CONST" # [invalid-all-format]
__all__ = ["Hello"] + {"world"} # [invalid-all-format]
__all__ += {"world"} # [invalid-all-format]
__all__ = {"world"} + ["Hello"] # [invalid-all-format]
__all__ = (x for x in ["Hello", "world"]) # [invalid-all-format]
__all__ = {x for x in ["Hello", "world"]} # [invalid-all-format]
__all__ = ["Hello"]
__all__ = ("Hello",)
__all__ = ["Hello"] + ("world",)
__all__ = [x for x in ["Hello", "world"]]

View File

@@ -0,0 +1,11 @@
__all__ = (
None, # [invalid-all-object]
Fruit,
Worm,
)
class Fruit:
pass
class Worm:
pass

View File

@@ -47,7 +47,7 @@ if input_password == "": # correct
if input_password == ADMIN_PASSWORD: # correct
pass
if input_password == "Hunter2": # [magic-value-comparison]
if input_password == "Hunter2": # correct
pass
PI = 3.141592653589793238
@@ -62,7 +62,7 @@ if pi_estimation == PI: # correct
HELLO_WORLD = b"Hello, World!"
user_input = b"Hello, There!"
if user_input == b"something": # [magic-value-comparison]
if user_input == b"something": # correct
pass
if user_input == HELLO_WORLD: # correct

View File

@@ -0,0 +1,34 @@
def f(x, y, z, t, u, v, w, r): # Too many arguments (8/5)
pass
def f(x, y, z, t, u): # OK
pass
def f(x): # OK
pass
def f(x, y, z, _t, _u, _v, _w, r): # OK (underscore-prefixed names are ignored
pass
def f(x, y, z, u=1, v=1, r=1): # Too many arguments (6/5)
pass
def f(x=1, y=1, z=1): # OK
pass
def f(x, y, z, /, u, v, w): # OK
pass
def f(x, y, z, *, u, v, w): # OK
pass
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (6/5)
pass

View File

@@ -0,0 +1,10 @@
# Too many args (6/4) for max_args=4
# OK for dummy_variable_rgx ~ "skip_.*"
def f(x, y, z, skip_t, skip_u, skip_v):
pass
# Too many args (6/4) for max_args=4
# Too many args (6/5) for dummy_variable_rgx ~ "skip_.*"
def f(x, y, z, t, u, v):
pass

View File

@@ -1,87 +0,0 @@
# Replace names by built-in names, whether namespaced or not
# https://github.com/search?q=%22from+six+import%22&type=code
import six
from six.moves import map # No need
from six import text_type
six.text_type # str
six.binary_type # bytes
six.class_types # (type,)
six.string_types # (str,)
six.integer_types # (int,)
six.unichr # chr
six.iterbytes # iter
six.print_(...) # print(...)
six.exec_(c, g, l) # exec(c, g, l)
six.advance_iterator(it) # next(it)
six.next(it) # next(it)
six.callable(x) # callable(x)
six.moves.range(x) # range(x)
six.moves.xrange(x) # range(x)
isinstance(..., six.class_types) # isinstance(..., type)
issubclass(..., six.integer_types) # issubclass(..., int)
isinstance(..., six.string_types) # isinstance(..., str)
# Replace call on arg by method call on arg
six.iteritems(dct) # dct.items()
six.iterkeys(dct) # dct.keys()
six.itervalues(dct) # dct.values()
six.viewitems(dct) # dct.items()
six.viewkeys(dct) # dct.keys()
six.viewvalues(dct) # dct.values()
six.assertCountEqual(self, a1, a2) # self.assertCountEqual(a1, a2)
six.assertRaisesRegex(self, e, r, fn) # self.assertRaisesRegex(e, r, fn)
six.assertRegex(self, s, r) # self.assertRegex(s, r)
# Replace call on arg by arg attribute
six.get_method_function(meth) # meth.__func__
six.get_method_self(meth) # meth.__self__
six.get_function_closure(fn) # fn.__closure__
six.get_function_code(fn) # fn.__code__
six.get_function_defaults(fn) # fn.__defaults__
six.get_function_globals(fn) # fn.__globals__
# Replace by string literal
six.b("...") # b'...'
six.u("...") # '...'
six.ensure_binary("...") # b'...'
six.ensure_str("...") # '...'
six.ensure_text("...") # '...'
six.b(string) # no change
# Replace by simple expression
six.get_unbound_function(meth) # meth
six.create_unbound_method(fn, cls) # fn
# Raise exception
six.raise_from(exc, exc_from) # raise exc from exc_from
six.reraise(tp, exc, tb) # raise exc.with_traceback(tb)
six.reraise(*sys.exc_info()) # raise
# Int / Bytes conversion
six.byte2int(bs) # bs[0]
six.indexbytes(bs, i) # bs[i]
six.int2byte(i) # bytes((i, ))
# Special cases for next calls
next(six.iteritems(dct)) # next(iter(dct.items()))
next(six.iterkeys(dct)) # next(iter(dct.keys()))
next(six.itervalues(dct)) # next(iter(dct.values()))
# TODO: To implement
# Rewrite classes
@six.python_2_unicode_compatible # Remove
class C(six.Iterator):
pass # class C: pass
class C(six.with_metaclass(M, B)):
pass # class C(B, metaclass=M): pass
# class C(B, metaclass=M): pass
@six.add_metaclass(M)
class C(B):
pass

View File

@@ -0,0 +1 @@
import itertools # noqa: F401

View File

@@ -0,0 +1,31 @@
"""
Violation:
Raising vanilla exception with custom message means it should be customized.
"""
from somewhere import exceptions
def func():
a = 1
if a == 1:
raise Exception("Custom message")
b = 1
if b == 1:
raise Exception
def ignore():
try:
a = 1
except Exception as ex:
# This is another violation, but this specific analyzer shouldn't care
raise ex
def anotherfunc():
a = 1
if a == 1:
raise exceptions.Exception("Another except") # That's fine

View File

@@ -0,0 +1,54 @@
class CustomException(Exception):
pass
def func():
a = 1
if a == 1:
raise CustomException("Long message")
elif a == 2:
raise CustomException("Short") # This is acceptable
elif a == 3:
raise CustomException("its_code_not_message") # This is acceptable
def ignore():
try:
a = 1
except Exception as ex:
# This is another violation, but this specific analyzer shouldn't care
raise ex
class BadArgCantBeEven(Exception):
pass
class GoodArgCantBeEven(Exception):
def __init__(self, arg):
super().__init__(f"The argument '{arg}' should be even")
def bad(a):
if a % 2 == 0:
raise BadArgCantBeEven(f"The argument '{a}' should be even")
def another_bad(a):
if a % 2 == 0:
raise BadArgCantBeEven(f"The argument {a} should not be odd.")
def and_another_bad(a):
if a % 2 == 0:
raise BadArgCantBeEven("The argument `a` should not be odd.")
def good(a: int):
if a % 2 == 0:
raise GoodArgCantBeEven(a)
def another_good(a):
if a % 2 == 0:
raise GoodArgCantBeEven(a)

View File

@@ -35,6 +35,16 @@ def still_good():
raise
def still_actually_good():
try:
process()
except MyException as e:
try:
pass
except TypeError:
raise e
def bad_that_needs_recursion():
try:
process()

View File

@@ -0,0 +1,62 @@
"""
Violation:
Use '.exception' over '.error' inside except blocks
"""
import logging
logger = logging.getLogger(__name__)
def bad():
try:
a = 1
except Exception:
logging.error("Context message here")
if True:
logging.error("Context message here")
def bad():
try:
a = 1
except Exception:
logger.error("Context message here")
if True:
logger.error("Context message here")
def bad():
try:
a = 1
except Exception:
log.error("Context message here")
if True:
log.error("Context message here")
def bad():
try:
a = 1
except Exception:
self.logger.error("Context message here")
if True:
self.logger.error("Context message here")
def good():
try:
a = 1
except Exception:
logger.exception("Context message here")
def good():
try:
a = 1
except Exception:
foo.exception("Context message here")

View File

@@ -66,18 +66,8 @@
"type": "string"
}
},
"extend-ignore": {
"description": "A list of rule codes or prefixes to ignore, in addition to those specified by `ignore`.\n\nNote that `extend-ignore` is applied after resolving rules from `ignore`/`select` and a less specific rule in `extend-ignore` would overwrite a more specific rule in `select`. It is recommended to only use `extend-ignore` when extending a `pyproject.toml` file via `extend`.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/RuleSelector"
}
},
"extend-select": {
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.\n\nNote that `extend-select` is applied after resolving rules from `ignore`/`select` and a less specific rule in `extend-select` would overwrite a more specific rule in `ignore`. It is recommended to only use `extend-select` when extending a `pyproject.toml` file via `extend`.",
"description": "A list of rule codes or prefixes to enable, in addition to those specified by `select`.",
"type": [
"array",
"null"
@@ -111,7 +101,7 @@
]
},
"fixable": {
"description": "A list of rule codes or prefixes to consider autofixable.",
"description": "A list of rule codes or prefixes to consider autofixable. By default, all rules are considered autofixable.",
"type": [
"array",
"null"
@@ -230,6 +220,17 @@
}
]
},
"flake8-type-checking": {
"description": "Options for the `flake8-type-checking` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8TypeCheckingOptions"
},
{
"type": "null"
}
]
},
"flake8-unused-arguments": {
"description": "Options for the `flake8-unused-arguments` plugin.",
"anyOf": [
@@ -421,7 +422,7 @@
]
},
"src": {
"description": "The source code paths to consider, e.g., when resolving first- vs. third-party imports.\n\nAs an example: given a Python package structure like:\n\n```text my_package/ pyproject.toml src/ my_package/ __init__.py foo.py bar.py ```\n\nThe `src` directory should be included in `source` (e.g., `source = [\"src\"]`), such that when resolving imports, `my_package.foo` is considered a first-party import.\n\nThis field supports globs. For example, if you have a series of Python packages in a `python_modules` directory, `src = [\"python_modules/*\"]` would expand to incorporate all of the packages in that directory. User home directory and environment variables will also be expanded.",
"description": "The source code paths to consider, e.g., when resolving first- vs. third-party imports.\n\nAs an example: given a Python package structure like:\n\n```text my_package/ pyproject.toml src/ my_package/ __init__.py foo.py bar.py ```\n\nThe `src` directory should be included in the `src` option (e.g., `src = [\"src\"]`), such that when resolving imports, `my_package.foo` is considered a first-party import.\n\nThis field supports globs. For example, if you have a series of Python packages in a `python_modules` directory, `src = [\"python_modules/*\"]` would expand to incorporate all of the packages in that directory. User home directory and environment variables will also be expanded.",
"type": [
"array",
"null"
@@ -567,6 +568,13 @@
"Flake8BanditOptions": {
"type": "object",
"properties": {
"check-typed-exception": {
"description": "Whether to disallow `try`-`except`-`pass` (`S110`) for specific exception types. By default, `try`-`except`-`pass` is only disallowed for `Exception` and `BaseException`.",
"type": [
"boolean",
"null"
]
},
"hardcoded-tmp-directory": {
"description": "A list of directories to consider temporary.",
"type": [
@@ -641,7 +649,7 @@
"type": "object",
"properties": {
"allow-multiline": {
"description": "Whether to allow implicit string concatenations for multiline strings. By default, implicit concatenations of multiline strings are allowed (but continuation lines, delimited with a backslash, are prohibited).",
"description": "Whether to allow implicit string concatenations for multiline strings. By default, implicit concatenations of multiline strings are allowed (but continuation lines, delimited with a backslash, are prohibited).\n\nNote that setting `allow-multiline = false` should typically be coupled with disabling `explicit-string-concatenation` (`ISC003`). Otherwise, both explicit and implicit multiline string concatenations will be seen as violations.",
"type": [
"boolean",
"null"
@@ -822,6 +830,29 @@
},
"additionalProperties": false
},
"Flake8TypeCheckingOptions": {
"type": "object",
"properties": {
"exempt-modules": {
"description": "Exempt certain modules from needing to be moved into type-checking blocks.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"strict": {
"description": "Enforce TC001, TC002, and TC003 rules even when valid runtime imports are present for the same module. See: https://github.com/snok/flake8-type-checking#strict.",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"Flake8UnusedArgumentsOptions": {
"type": "object",
"properties": {
@@ -947,7 +978,7 @@
"description": "Whether to place \"closer\" imports (fewer `.` characters, most local) before \"further\" imports (more `.` characters, least local), or vice versa.\n\nThe default (\"furthest-to-closest\") is equivalent to isort's `reverse-relative` default (`reverse-relative = false`); setting this to \"closest-to-furthest\" is equivalent to isort's `reverse-relative = true`.",
"anyOf": [
{
"$ref": "#/definitions/RelatveImportsOrder"
"$ref": "#/definitions/RelativeImportsOrder"
},
{
"type": "null"
@@ -1123,7 +1154,7 @@
"type": "object",
"properties": {
"allow-magic-value-types": {
"description": "Constant types to ignore when used as \"magic values\".",
"description": "Constant types to ignore when used as \"magic values\" (see: `PLR2004`).",
"type": [
"array",
"null"
@@ -1131,6 +1162,15 @@
"items": {
"$ref": "#/definitions/ConstantType"
}
},
"max-args": {
"description": "Maximum number of arguments allowed for a function definition (see: `PLR0913`).",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
@@ -1163,7 +1203,7 @@
}
]
},
"RelatveImportsOrder": {
"RelativeImportsOrder": {
"oneOf": [
{
"description": "Place \"closer\" imports (fewer `.` characters, most local) before \"further\" imports (more `.` characters, least local).",
@@ -1475,6 +1515,22 @@
"FBT001",
"FBT002",
"FBT003",
"G",
"G0",
"G00",
"G001",
"G002",
"G003",
"G004",
"G01",
"G010",
"G1",
"G10",
"G101",
"G2",
"G20",
"G201",
"G202",
"I",
"I0",
"I00",
@@ -1565,6 +1621,10 @@
"PLE011",
"PLE0117",
"PLE0118",
"PLE06",
"PLE060",
"PLE0604",
"PLE0605",
"PLE1",
"PLE11",
"PLE114",
@@ -1580,6 +1640,9 @@
"PLR04",
"PLR040",
"PLR0402",
"PLR09",
"PLR091",
"PLR0913",
"PLR1",
"PLR17",
"PLR170",
@@ -1699,6 +1762,7 @@
"S107",
"S108",
"S11",
"S110",
"S113",
"S3",
"S32",
@@ -1776,6 +1840,8 @@
"TRY",
"TRY0",
"TRY00",
"TRY002",
"TRY003",
"TRY004",
"TRY2",
"TRY20",
@@ -1785,6 +1851,9 @@
"TRY30",
"TRY300",
"TRY301",
"TRY4",
"TRY40",
"TRY400",
"UP",
"UP0",
"UP00",
@@ -1803,7 +1872,6 @@
"UP013",
"UP014",
"UP015",
"UP016",
"UP017",
"UP018",
"UP019",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.235"
version = "0.0.238"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -53,6 +53,7 @@ similar = { version = "2.2.1" }
textwrap = { version = "0.16.0" }
update-informer = { version = "0.6.0", default-features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
strum = "0.24.1"
[dev-dependencies]
assert_cmd = { version = "2.0.4" }

View File

@@ -5,6 +5,7 @@ use regex::Regex;
use ruff::logging::LogLevel;
use ruff::registry::Rule;
use ruff::resolver::ConfigProcessor;
use ruff::settings::configuration::RuleSelection;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
};
@@ -15,201 +16,238 @@ use rustc_hash::FxHashMap;
#[command(
author,
name = "ruff",
about = "Ruff: An extremely fast Python linter."
about = "Ruff: An extremely fast Python linter.",
after_help = "For help with a specific command, see: `ruff help <command>`."
)]
#[command(version)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
#[arg(required_unless_present_any = ["clean", "explain", "generate_shell_completion"])]
pub struct Args {
#[command(subcommand)]
pub command: Command,
#[clap(flatten)]
pub log_level_args: LogLevelArgs,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Run Ruff on the given files or directories (default).
Check(CheckArgs),
/// Explain a rule.
#[clap(alias = "--explain")]
Rule {
#[arg(value_parser=Rule::from_code)]
rule: &'static Rule,
/// Output format
#[arg(long, value_enum, default_value = "text")]
format: HelpFormat,
},
/// List all supported upstream linters
Linter {
/// Output format
#[arg(long, value_enum, default_value = "text")]
format: HelpFormat,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
Clean,
/// Generate shell completion.
#[clap(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
}
#[derive(Debug, clap::Args)]
#[allow(clippy::struct_excessive_bools, clippy::module_name_repetitions)]
pub struct CheckArgs {
/// List of files or directories to check.
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Enable verbose logging.
#[arg(short, long, group = "verbosity")]
pub verbose: bool,
/// Print lint violations, but nothing else.
#[arg(short, long, group = "verbosity")]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting
/// lint violations).
#[arg(short, long, group = "verbosity")]
pub silent: bool,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(short, long)]
pub exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Attempt to automatically fix lint violations.
#[arg(long, overrides_with("no_fix"))]
fix: bool,
#[clap(long, overrides_with("fix"), hide = true)]
no_fix: bool,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Avoid writing any fixed files back; instead, output a diff for each
/// changed file to stdout.
#[arg(long)]
pub diff: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Fix any fixable lint violations, but don't report on leftover
/// violations. Implies `--fix`.
#[arg(long, overrides_with("no_fix_only"))]
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
no_fix_only: bool,
/// Avoid writing any fixed files back; instead, output a diff for each
/// changed file to stdout.
#[arg(long)]
pub diff: bool,
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config")]
pub isolated: bool,
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub select: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of
/// those already excluded.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub unfixable: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Output serialization format for violations.
#[arg(long, value_enum, env = "RUFF_FORMAT")]
pub format: Option<SerializationFormat>,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<PathBuf>,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR")]
pub cache_dir: Option<PathBuf>,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub extend_select: Option<Vec<RuleSelector>>,
/// Like --ignore. (Deprecated: You can just use --ignore instead.)
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection",
hide = true
)]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of
/// those already excluded.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
help_heading = "Rule selection"
)]
pub unfixable: Option<Vec<RuleSelector>>,
/// Respect file exclusions via `.gitignore` and other standard ignore
/// files.
#[arg(long, overrides_with("no_respect_gitignore"))]
#[arg(
long,
overrides_with("no_respect_gitignore"),
help_heading = "File selection"
)]
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// Enforce exclusions, even for paths passed to Ruff directly on the
/// command-line.
#[arg(long, overrides_with("no_force_exclude"))]
#[arg(
long,
overrides_with("no_force_exclude"),
help_heading = "File selection"
)]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Enable or disable automatic update checks.
#[arg(long, overrides_with("no_update_check"))]
update_check: bool,
#[clap(long, overrides_with("update_check"), hide = true)]
no_update_check: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
pub dummy_variable_rgx: Option<Regex>,
/// The minimum Python version that should be supported.
#[arg(long)]
#[arg(long, help_heading = "Rule configuration")]
pub target_version: Option<PythonVersion>,
/// Set the line-length for length-associated rules and automatic
/// formatting.
#[arg(long)]
#[arg(long, help_heading = "Rule configuration")]
pub line_length: Option<usize>,
/// Regular expression matching the name of dummy variables.
#[arg(long, help_heading = "Rule configuration")]
pub dummy_variable_rgx: Option<Regex>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>,
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(short, long, help_heading = "Miscellaneous")]
pub exit_zero: bool,
/// Enable or disable automatic update checks.
#[arg(
long,
overrides_with("no_update_check"),
help_heading = "Miscellaneous"
)]
update_check: bool,
#[clap(long, overrides_with("update_check"), hide = true)]
no_update_check: bool,
/// Show counts for every rule with at least one violation.
#[arg(
long,
// Unsupported default-command arguments.
conflicts_with = "diff",
conflicts_with = "show_source",
conflicts_with = "watch",
)]
pub statistics: bool,
/// Enable automatic additions of `noqa` directives to failing lines.
#[arg(
long,
// conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub add_noqa: bool,
/// Clear any caches in the current directory or any subdirectories.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
// conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub clean: bool,
/// Explain a rule.
#[arg(
long,
value_parser=Rule::from_code,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
// conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub explain: Option<&'static Rule>,
/// Generate shell completion
#[arg(
long,
hide = true,
value_name = "SHELL",
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
// conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub generate_shell_completion: Option<clap_complete_command::Shell>,
/// See the files Ruff will be run against with the current settings.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
// conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
@@ -219,40 +257,86 @@ pub struct Cli {
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
// conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub show_settings: bool,
}
impl Cli {
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HelpFormat {
Text,
Json,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub verbose: bool,
/// Print lint violations, but nothing else.
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting
/// lint violations).
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub silent: bool,
}
impl From<&LogLevelArgs> for LogLevel {
fn from(args: &LogLevelArgs) -> Self {
if args.silent {
LogLevel::Silent
} else if args.quiet {
LogLevel::Quiet
} else if args.verbose {
LogLevel::Verbose
} else {
LogLevel::Default
}
}
}
impl CheckArgs {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> (Arguments, Overrides) {
(
Arguments {
add_noqa: self.add_noqa,
clean: self.clean,
config: self.config,
diff: self.diff,
exit_zero: self.exit_zero,
explain: self.explain,
files: self.files,
generate_shell_completion: self.generate_shell_completion,
isolated: self.isolated,
no_cache: self.no_cache,
quiet: self.quiet,
show_files: self.show_files,
show_settings: self.show_settings,
silent: self.silent,
statistics: self.statistics,
stdin_filename: self.stdin_filename,
verbose: self.verbose,
watch: self.watch,
},
Overrides {
@@ -299,21 +383,16 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
#[allow(clippy::struct_excessive_bools)]
pub struct Arguments {
pub add_noqa: bool,
pub clean: bool,
pub config: Option<PathBuf>,
pub diff: bool,
pub exit_zero: bool,
pub explain: Option<&'static Rule>,
pub files: Vec<PathBuf>,
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub isolated: bool,
pub no_cache: bool,
pub quiet: bool,
pub show_files: bool,
pub show_settings: bool,
pub silent: bool,
pub statistics: bool,
pub stdin_filename: Option<PathBuf>,
pub verbose: bool,
pub watch: bool,
}
@@ -364,18 +443,25 @@ impl ConfigProcessor for &Overrides {
if let Some(fix_only) = &self.fix_only {
config.fix_only = Some(*fix_only);
}
if let Some(fixable) = &self.fixable {
config.fixable = Some(fixable.clone());
}
config.rule_selections.push(RuleSelection {
select: self.select.clone(),
ignore: self
.ignore
.iter()
.cloned()
.chain(self.extend_ignore.iter().cloned().into_iter())
.flatten()
.collect(),
extend_select: self.extend_select.clone().unwrap_or_default(),
fixable: self.fixable.clone(),
unfixable: self.unfixable.clone().unwrap_or_default(),
});
if let Some(format) = &self.format {
config.format = Some(*format);
}
if let Some(force_exclude) = &self.force_exclude {
config.force_exclude = Some(*force_exclude);
}
if let Some(ignore) = &self.ignore {
config.ignore = Some(ignore.clone());
}
if let Some(line_length) = &self.line_length {
config.line_length = Some(*line_length);
}
@@ -385,51 +471,15 @@ impl ConfigProcessor for &Overrides {
if let Some(respect_gitignore) = &self.respect_gitignore {
config.respect_gitignore = Some(*respect_gitignore);
}
if let Some(select) = &self.select {
config.select = Some(select.clone());
}
if let Some(show_source) = &self.show_source {
config.show_source = Some(*show_source);
}
if let Some(target_version) = &self.target_version {
config.target_version = Some(*target_version);
}
if let Some(unfixable) = &self.unfixable {
config.unfixable = Some(unfixable.clone());
}
if let Some(update_check) = &self.update_check {
config.update_check = Some(*update_check);
}
// Special-case: `extend_ignore` and `extend_select` are parallel arrays, so
// push an empty array if only one of the two is provided.
match (&self.extend_ignore, &self.extend_select) {
(Some(extend_ignore), Some(extend_select)) => {
config.extend_ignore.push(extend_ignore.clone());
config.extend_select.push(extend_select.clone());
}
(Some(extend_ignore), None) => {
config.extend_ignore.push(extend_ignore.clone());
config.extend_select.push(Vec::new());
}
(None, Some(extend_select)) => {
config.extend_ignore.push(Vec::new());
config.extend_select.push(extend_select.clone());
}
(None, None) => {}
}
}
}
/// Map the CLI settings to a `LogLevel`.
pub fn extract_log_level(cli: &Arguments) -> LogLevel {
if cli.silent {
LogLevel::Silent
} else if cli.quiet {
LogLevel::Quiet
} else if cli.verbose {
LogLevel::Verbose
} else {
LogLevel::Default
}
}

View File

@@ -16,31 +16,30 @@ use ruff::linter::add_noqa_to_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::{Linter, Rule, RuleNamespace};
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
use ruff::resolver::PyprojectDiscovery;
use ruff::settings::flags;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, fs, packaging, resolver, warn_user_once, AutofixAvailability, IOError};
use serde::Serialize;
use walkdir::WalkDir;
use crate::args::{HelpFormat, Overrides};
use crate::cache;
use crate::cli::Overrides;
use crate::diagnostics::{lint_path, lint_stdin, Diagnostics};
use crate::iterators::par_iter;
pub mod linter;
/// Run the linter over a collection of files.
pub fn run(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
cache: flags::Cache,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Collect all the Python files to check.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -49,9 +48,6 @@ pub fn run(
return Ok(Diagnostics::default());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
// Initialize the cache.
if matches!(cache, flags::Cache::Enabled) {
match &pyproject_strategy {
@@ -116,7 +112,7 @@ pub fn run(
let settings = resolver.resolve(path, pyproject_strategy);
if settings.rules.enabled(&Rule::IOError) {
Diagnostics::new(vec![Message {
kind: IOError(message).into(),
kind: IOError { message }.into(),
location: Location::default(),
end_location: Location::default(),
fix: None,
@@ -156,12 +152,11 @@ fn read_from_stdin() -> Result<String> {
pub fn run_stdin(
filename: Option<&Path>,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
if let Some(filename) = filename {
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
if !resolver::python_file_at_path(filename, pyproject_strategy, overrides)? {
return Ok(Diagnostics::default());
}
}
@@ -182,13 +177,11 @@ pub fn run_stdin(
pub fn add_noqa(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -197,9 +190,6 @@ pub fn add_noqa(
return Ok(0);
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.flatten()
@@ -226,15 +216,10 @@ pub fn add_noqa(
pub fn show_settings(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let (paths, resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
// Print the list of files.
let Some(entry) = paths
@@ -255,21 +240,16 @@ pub fn show_settings(
pub fn show_files(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let (paths, _resolver) = resolver::python_files_in_path(files, pyproject_strategy, overrides)?;
if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)");
return Ok(());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
// Print the list of files.
for entry in paths
.iter()
@@ -290,10 +270,10 @@ struct Explanation<'a> {
}
/// Explain a `Rule` to the user.
pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
pub fn rule(rule: &Rule, format: HelpFormat) -> Result<()> {
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
match format {
SerializationFormat::Text | SerializationFormat::Grouped => {
HelpFormat::Text => {
println!("{}\n", rule.as_ref());
println!("Code: {} ({})\n", rule.code(), linter.name());
@@ -311,7 +291,7 @@ pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
println!("* {format}");
}
}
SerializationFormat::Json => {
HelpFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&Explanation {
@@ -321,24 +301,12 @@ pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
})?
);
}
SerializationFormat::Junit => {
bail!("`--explain` does not support junit format")
}
SerializationFormat::Github => {
bail!("`--explain` does not support GitHub format")
}
SerializationFormat::Gitlab => {
bail!("`--explain` does not support GitLab format")
}
SerializationFormat::Pylint => {
bail!("`--explain` does not support pylint format")
}
};
Ok(())
}
/// Clear any caches in the current directory or any subdirectories.
pub fn clean(level: &LogLevel) -> Result<()> {
pub fn clean(level: LogLevel) -> Result<()> {
for entry in WalkDir::new(&*path_dedot::CWD)
.into_iter()
.filter_map(Result::ok)
@@ -346,7 +314,7 @@ pub fn clean(level: &LogLevel) -> Result<()> {
{
let cache = entry.path().join(CACHE_DIR_NAME);
if cache.is_dir() {
if level >= &LogLevel::Default {
if level >= LogLevel::Default {
eprintln!("Removing cache at: {}", fs::relativize_path(&cache).bold());
}
remove_dir_all(&cache)?;

View File

@@ -0,0 +1,59 @@
use itertools::Itertools;
use serde::Serialize;
use strum::IntoEnumIterator;
use ruff::registry::{Linter, LinterCategory, RuleNamespace};
use crate::args::HelpFormat;
pub fn linter(format: HelpFormat) {
match format {
HelpFormat::Text => {
for linter in Linter::iter() {
let prefix = match linter.common_prefix() {
"" => linter
.categories()
.unwrap()
.iter()
.map(|LinterCategory(prefix, ..)| prefix)
.join("/"),
prefix => prefix.to_string(),
};
println!("{:>4} {}", prefix, linter.name());
}
}
HelpFormat::Json => {
let linters: Vec<_> = Linter::iter()
.map(|linter_info| LinterInfo {
prefix: linter_info.common_prefix(),
name: linter_info.name(),
categories: linter_info.categories().map(|cats| {
cats.iter()
.map(|LinterCategory(prefix, name, ..)| LinterCategoryInfo {
prefix,
name,
})
.collect()
}),
})
.collect();
println!("{}", serde_json::to_string_pretty(&linters).unwrap());
}
}
}
#[derive(Serialize)]
struct LinterInfo {
prefix: &'static str,
name: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
categories: Option<Vec<LinterCategoryInfo>>,
}
#[derive(Serialize)]
struct LinterCategoryInfo {
prefix: &'static str,
name: &'static str,
}

View File

@@ -42,9 +42,6 @@ pub fn lint_path(
cache: flags::Cache,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.lib.validate()?;
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
// side-effects that aren't captured in the cache. (In practice, it's fine
@@ -116,9 +113,6 @@ pub fn lint_stdin(
settings: &Settings,
autofix: fix::FixMode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Lint the inputs.
let (messages, fixed) = if matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff) {
let (transformed, fixed, messages) = lint_fix(

View File

@@ -1,16 +1,30 @@
//! This library only exists to enable the Ruff internal tooling (`ruff_dev`)
//! to automatically update the `ruff --help` output in the `README.md`.
//! to automatically update the `ruff help` output in the `README.md`.
//!
//! For the actual Ruff library, see [`ruff`].
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate, dead_code)]
mod cli;
mod args;
use clap::CommandFactory;
/// Returns the output of `ruff --help`.
pub fn help() -> String {
cli::Cli::command().render_help().to_string()
/// Returns the output of `ruff help`.
pub fn command_help() -> String {
args::Args::command().render_help().to_string()
}
/// Returns the output of `ruff help check`.
pub fn subcommand_help() -> String {
let mut cmd = args::Args::command();
// The build call is necessary for the help output to contain `Usage: ruff check` instead of `Usage: check`
// see https://github.com/clap-rs/clap/issues/4685
cmd.build();
cmd.find_subcommand_mut("check")
.expect("`check` subcommand not found")
.render_help()
.to_string()
}

View File

@@ -8,90 +8,55 @@
)]
use std::io::{self};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::mpsc::channel;
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::resolver::{
resolve_settings_with_processor, ConfigProcessor, FileDiscovery, PyprojectDiscovery, Relativity,
};
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::pyproject;
use ::ruff::resolver::PyprojectDiscovery;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::{fix, fs, warn_user_once};
use anyhow::Result;
use clap::{CommandFactory, Parser};
use cli::{extract_log_level, Cli, Overrides};
use args::{Args, CheckArgs, Command};
use clap::{CommandFactory, Parser, Subcommand};
use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
use printer::{Printer, Violations};
use ruff::settings::{AllSettings, CliSettings};
use ruff::settings::CliSettings;
pub(crate) mod args;
mod cache;
mod cli;
mod commands;
mod diagnostics;
mod iterators;
mod printer;
mod resolve;
#[cfg(all(feature = "update-informer"))]
pub mod updates;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(
isolated: bool,
config: Option<&Path>,
overrides: &Overrides,
stdin_filename: Option<&Path>,
) -> Result<PyprojectDiscovery> {
if isolated {
// First priority: if we're running in isolated mode, use the default settings.
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = config {
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings_with_processor(pyproject, &Relativity::Cwd, overrides)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_settings_toml(
stdin_filename
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
// so these act as the "default" settings.)
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else {
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}
}
fn inner_main() -> Result<ExitCode> {
let mut args: Vec<_> = std::env::args_os().collect();
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
if let Some(arg) = args.get(1).and_then(|s| s.to_str()) {
if !Command::has_subcommand(rewrite_legacy_subcommand(arg))
&& arg != "-h"
&& arg != "--help"
&& arg != "-V"
&& arg != "--version"
&& arg != "help"
{
args.insert(1, "check".into());
}
}
// Extract command-line arguments.
let (cli, overrides) = Cli::parse().partition();
let Args {
command,
log_level_args,
} = Args::parse_from(args);
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
@@ -108,45 +73,45 @@ quoting the executed command, along with the relevant file contents and `pyproje
default_panic_hook(info);
}));
let log_level = extract_log_level(&cli);
let log_level: LogLevel = (&log_level_args).into();
set_up_logging(&log_level)?;
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS);
}
if cli.clean {
commands::clean(&log_level)?;
return Ok(ExitCode::SUCCESS);
match command {
Command::Rule { rule, format } => commands::rule(rule, format)?,
Command::Linter { format } => commands::linter::linter(format),
Command::Clean => commands::clean(log_level)?,
Command::GenerateShellCompletion { shell } => {
shell.generate(&mut Args::command(), &mut io::stdout());
}
Command::Check(args) => return check(args, log_level),
}
Ok(ExitCode::SUCCESS)
}
fn check(args: CheckArgs, log_level: LogLevel) -> Result<ExitCode> {
let (cli, overrides) = args.partition();
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let pyproject_strategy = resolve(
let pyproject_strategy = resolve::resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
// Validate the `Settings` and return any errors.
match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.validate()?,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.validate()?,
};
if cli.show_settings {
commands::show_settings(&cli.files, &pyproject_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &pyproject_strategy, &overrides)?;
}
// Extract options that are included in `Settings`, but only apply at the top
// level.
let file_strategy = FileDiscovery {
force_exclude: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.force_exclude,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.force_exclude,
},
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.lib.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.lib.respect_gitignore,
},
};
let CliSettings {
fix,
fix_only,
@@ -158,19 +123,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
PyprojectDiscovery::Hierarchical(settings) => settings.cli.clone(),
};
if let Some(rule) = cli.explain {
commands::explain(rule, format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
// Autofix rules are as follows:
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
// print them to stdout, if we're reading from stdin).
@@ -204,7 +156,16 @@ quoting the executed command, along with the relevant file contents and `pyproje
warn_user_once!("Detected debug build without --no-cache.");
}
if cli.add_noqa {
let modifications = commands::add_noqa(&cli.files, &pyproject_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
return Ok(ExitCode::SUCCESS);
}
let printer = Printer::new(&format, &log_level, &autofix, &violations);
if cli.watch {
if !matches!(autofix, fix::FixMode::None) {
warn_user_once!("--fix is not enabled in watch mode.");
@@ -220,7 +181,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
fix::FixMode::None,
@@ -250,7 +210,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
let messages = commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
fix::FixMode::None,
@@ -261,12 +220,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
Err(err) => return Err(err.into()),
}
}
} else if cli.add_noqa {
let modifications =
commands::add_noqa(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else {
let is_stdin = cli.files == vec![PathBuf::from("-")];
@@ -275,7 +228,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
commands::run_stdin(
cli.stdin_filename.map(fs::normalize_path).as_deref(),
&pyproject_strategy,
&file_strategy,
&overrides,
autofix,
)?
@@ -283,7 +235,6 @@ quoting the executed command, along with the relevant file contents and `pyproje
commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache.into(),
autofix,
@@ -294,7 +245,11 @@ quoting the executed command, along with the relevant file contents and `pyproje
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff)) {
printer.write_once(&diagnostics)?;
if cli.statistics {
printer.write_statistics(&diagnostics)?;
} else {
printer.write_once(&diagnostics)?;
}
}
// Check for updates if we're in a non-silent log level.
@@ -317,10 +272,18 @@ quoting the executed command, along with the relevant file contents and `pyproje
}
}
}
Ok(ExitCode::SUCCESS)
}
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
match cmd {
"--explain" => "rule",
"--clean" => "clean",
"--generate-shell-completion" => "generate-shell-completion",
cmd => cmd,
}
}
#[must_use]
pub fn main() -> ExitCode {
match inner_main() {

View File

@@ -43,6 +43,13 @@ struct ExpandedMessage<'a> {
filename: &'a str,
}
#[derive(Serialize)]
struct ExpandedStatistics<'a> {
count: usize,
code: &'a str,
message: String,
}
struct SerializeRuleAsCode<'a>(&'a Rule);
impl Serialize for SerializeRuleAsCode<'_> {
@@ -336,6 +343,77 @@ impl<'a> Printer<'a> {
Ok(())
}
pub fn write_statistics(&self, diagnostics: &Diagnostics) -> Result<()> {
let mut violations = diagnostics
.messages
.iter()
.map(|message| message.kind.rule())
.collect::<Vec<_>>();
violations.sort();
violations.dedup();
let statistics = violations
.iter()
.map(|rule| ExpandedStatistics {
code: rule.code(),
count: diagnostics
.messages
.iter()
.filter(|message| message.kind.rule() == *rule)
.count(),
message: diagnostics
.messages
.iter()
.find(|message| message.kind.rule() == *rule)
.map(|message| message.kind.body())
.unwrap(),
})
.collect::<Vec<_>>();
let mut stdout = BufWriter::new(io::stdout().lock());
match self.format {
SerializationFormat::Text => {
// Compute the maximum number of digits in the count and code, for all messages, to enable
// pretty-printing.
let count_width = num_digits(
statistics
.iter()
.map(|statistic| statistic.count)
.max()
.unwrap(),
);
let code_width = statistics
.iter()
.map(|statistic| statistic.code.len())
.max()
.unwrap();
// By default, we mimic Flake8's `--statistics` format.
for msg in statistics {
writeln!(
stdout,
"{:>count_width$}\t{:<code_width$}\t{}",
msg.count, msg.code, msg.message
)?;
}
return Ok(());
}
SerializationFormat::Json => {
writeln!(stdout, "{}", serde_json::to_string_pretty(&statistics)?)?;
}
_ => {
anyhow::bail!(
"Unsupported serialization format for statistics: {:?}",
self.format
)
}
}
stdout.flush()?;
Ok(())
}
pub fn write_continuously(&self, diagnostics: &Diagnostics) -> Result<()> {
if matches!(self.log_level, LogLevel::Silent) {
return Ok(());

68
ruff_cli/src/resolve.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::path::Path;
use anyhow::Result;
use path_absolutize::path_dedot;
use ruff::resolver::{
resolve_settings_with_processor, ConfigProcessor, PyprojectDiscovery, Relativity,
};
use ruff::settings::configuration::Configuration;
use ruff::settings::{pyproject, AllSettings};
use crate::args::Overrides;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
pub fn resolve(
isolated: bool,
config: Option<&Path>,
overrides: &Overrides,
stdin_filename: Option<&Path>,
) -> Result<PyprojectDiscovery> {
// First priority: if we're running in isolated mode, use the default settings.
if isolated {
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
return Ok(PyprojectDiscovery::Fixed(settings));
}
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config {
let settings = resolve_settings_with_processor(pyproject, &Relativity::Cwd, overrides)?;
return Ok(PyprojectDiscovery::Fixed(settings));
}
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
// so these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_settings_toml(
stdin_filename
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?;
return Ok(PyprojectDiscovery::Hierarchical(settings));
}
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_user_settings_toml() {
let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?;
return Ok(PyprojectDiscovery::Hierarchical(settings));
}
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
overrides.process_config(&mut config);
let settings = AllSettings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
}

View File

@@ -14,7 +14,7 @@ const BIN_NAME: &str = "ruff";
#[test]
fn test_stdin_success() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--format", "text"])
cmd.args(["-", "--format", "text", "--isolated"])
.write_stdin("")
.assert()
.success();
@@ -25,7 +25,7 @@ fn test_stdin_success() -> Result<()> {
fn test_stdin_error() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text"])
.args(["-", "--format", "text", "--isolated"])
.write_stdin("import os\n")
.assert()
.failure();
@@ -41,7 +41,14 @@ fn test_stdin_error() -> Result<()> {
fn test_stdin_filename() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--stdin-filename", "F401.py"])
.args([
"-",
"--format",
"text",
"--stdin-filename",
"F401.py",
"--isolated",
])
.write_stdin("import os\n")
.assert()
.failure();
@@ -58,7 +65,14 @@ fn test_stdin_filename() -> Result<()> {
fn test_stdin_json() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "json", "--stdin-filename", "F401.py"])
.args([
"-",
"--format",
"json",
"--stdin-filename",
"F401.py",
"--isolated",
])
.write_stdin("import os\n")
.assert()
.failure();
@@ -107,7 +121,7 @@ fn test_stdin_json() -> Result<()> {
fn test_stdin_autofix() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix"])
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import os\nimport sys\n\nprint(sys.version)\n")
.assert()
.success();
@@ -122,7 +136,7 @@ fn test_stdin_autofix() -> Result<()> {
fn test_stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix"])
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n")
.assert()
.failure();
@@ -137,7 +151,7 @@ fn test_stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<(
fn test_stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix"])
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import sys\n\nprint(sys.version)\n")
.assert()
.success();
@@ -152,7 +166,7 @@ fn test_stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()>
fn test_show_source() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--show-source"])
.args(["-", "--format", "text", "--show-source", "--isolated"])
.write_stdin("l = 1")
.assert()
.failure();
@@ -163,8 +177,26 @@ fn test_show_source() -> Result<()> {
#[test]
fn explain_status_codes() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "F401"]).assert().success();
cmd.args(["--explain", "F401"]).assert().success();
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "RUF404"]).assert().failure();
cmd.args(["--explain", "RUF404"]).assert().failure();
Ok(())
}
#[test]
fn show_statistics() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--select", "F401", "--statistics"])
.write_stdin("import sys\nimport os\n\nprint(os.getuid())\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?
.lines()
.last()
.unwrap(),
"1\tF401\t`sys` imported but unused"
);
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.235"
version = "0.0.238"
edition = "2021"
[dependencies]
@@ -15,7 +15,7 @@ rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
serde_json = { version = "1.0.91" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }

View File

@@ -1,29 +1,28 @@
//! Run all code and documentation generation steps.
use anyhow::Result;
use clap::Args;
use crate::{generate_cli_help, generate_json_schema, generate_options, generate_rules_table};
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated artifacts to stdout (rather than to the filesystem).
#[arg(long)]
dry_run: bool,
}
pub fn main(cli: &Cli) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Cli {
dry_run: cli.dry_run,
pub fn main(args: &Args) -> Result<()> {
generate_json_schema::main(&generate_json_schema::Args {
dry_run: args.dry_run,
})?;
generate_rules_table::main(&generate_rules_table::Cli {
dry_run: cli.dry_run,
generate_rules_table::main(&generate_rules_table::Args {
dry_run: args.dry_run,
})?;
generate_options::main(&generate_options::Cli {
dry_run: cli.dry_run,
generate_options::main(&generate_options::Args {
dry_run: args.dry_run,
})?;
generate_cli_help::main(&generate_cli_help::Cli {
dry_run: cli.dry_run,
generate_cli_help::main(&generate_cli_help::Args {
dry_run: args.dry_run,
})?;
Ok(())
}

View File

@@ -1,15 +1,17 @@
//! Generate CLI help.
use anyhow::Result;
use clap::Args;
use crate::utils::replace_readme_section;
use anyhow::Result;
use std::str;
const HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated cli help. -->";
const HELP_END_PRAGMA: &str = "<!-- End auto-generated cli help. -->";
const COMMAND_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated command help. -->";
const COMMAND_HELP_END_PRAGMA: &str = "<!-- End auto-generated command help. -->";
#[derive(Args)]
pub struct Cli {
const SUBCOMMAND_HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated subcommand help. -->";
const SUBCOMMAND_HELP_END_PRAGMA: &str = "<!-- End auto-generated subcommand help. -->";
#[derive(clap::Args)]
pub struct Args {
/// Write the generated help to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -19,16 +21,26 @@ fn trim_lines(s: &str) -> String {
s.lines().map(str::trim_end).collect::<Vec<_>>().join("\n")
}
pub fn main(cli: &Cli) -> Result<()> {
let output = trim_lines(ruff_cli::help().trim());
pub fn main(args: &Args) -> Result<()> {
// Generate `ruff help`.
let command_help = trim_lines(ruff_cli::command_help().trim());
if cli.dry_run {
print!("{output}");
// Generate `ruff help check`.
let subcommand_help = trim_lines(ruff_cli::subcommand_help().trim());
if args.dry_run {
print!("{command_help}");
print!("{subcommand_help}");
} else {
replace_readme_section(
&format!("```\n{output}\n```\n"),
HELP_BEGIN_PRAGMA,
HELP_END_PRAGMA,
&format!("```\n{command_help}\n```\n"),
COMMAND_HELP_BEGIN_PRAGMA,
COMMAND_HELP_END_PRAGMA,
)?;
replace_readme_section(
&format!("```\n{subcommand_help}\n```\n"),
SUBCOMMAND_HELP_BEGIN_PRAGMA,
SUBCOMMAND_HELP_END_PRAGMA,
)?;
}

View File

@@ -2,22 +2,21 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::settings::options::Options;
use schemars::schema_for;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `ruff.schema.json`).
#[arg(long)]
pub(crate) dry_run: bool,
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
if cli.dry_run {
if args.dry_run {
println!("{schema_string}");
} else {
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))

View File

@@ -1,7 +1,6 @@
//! Generate a Markdown-compatible listing of configuration options.
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
@@ -11,8 +10,8 @@ use crate::utils::replace_readme_section;
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated options sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated options sections. -->";
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -39,7 +38,7 @@ fn emit_field(output: &mut String, field: &OptionField, group_name: Option<&str>
output.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
let mut output = String::new();
// Generate all the top-level fields.
@@ -89,7 +88,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
if cli.dry_run {
if args.dry_run {
print!("{output}");
} else {
replace_readme_section(&output, BEGIN_PRAGMA, END_PRAGMA)?;

View File

@@ -1,7 +1,7 @@
//! Generate a Markdown-compatible table of supported lint rules.
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::registry::{Linter, LinterCategory, Rule, RuleNamespace};
use strum::IntoEnumIterator;
@@ -13,8 +13,8 @@ const TABLE_END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TOC_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated table of contents. -->";
const TOC_END_PRAGMA: &str = "<!-- End auto-generated table of contents. -->";
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
@@ -43,12 +43,20 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>)
table_out.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
pub fn main(args: &Args) -> Result<()> {
// Generate the table string.
let mut table_out = String::new();
let mut toc_out = String::new();
for linter in Linter::iter() {
let codes_csv: String = linter.prefixes().join(", ");
let codes_csv: String = match linter.common_prefix() {
"" => linter
.categories()
.unwrap()
.iter()
.map(|LinterCategory(prefix, ..)| prefix)
.join(", "),
prefix => prefix.to_string(),
};
table_out.push_str(&format!("### {} ({codes_csv})", linter.name()));
table_out.push('\n');
table_out.push('\n');
@@ -96,7 +104,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
if cli.dry_run {
if args.dry_run {
print!("Table of Contents: {toc_out}\n Rules Tables: {table_out}");
} else {
// Extra newline in the markdown numbered list looks weird

View File

@@ -33,45 +33,45 @@ use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
struct Args {
#[command(subcommand)]
command: Commands,
command: Command,
}
#[derive(Subcommand)]
enum Commands {
enum Command {
/// Run all code and documentation generation steps.
GenerateAll(generate_all::Cli),
GenerateAll(generate_all::Args),
/// Generate JSON schema for the TOML configuration file.
GenerateJSONSchema(generate_json_schema::Cli),
GenerateJSONSchema(generate_json_schema::Args),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable(generate_rules_table::Cli),
GenerateRulesTable(generate_rules_table::Args),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions(generate_options::Cli),
GenerateOptions(generate_options::Args),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Cli),
GenerateCliHelp(generate_cli_help::Args),
/// Print the AST for a given Python file.
PrintAST(print_ast::Cli),
PrintAST(print_ast::Args),
/// Print the LibCST CST for a given Python file.
PrintCST(print_cst::Cli),
PrintCST(print_cst::Args),
/// Print the token stream for a given Python file.
PrintTokens(print_tokens::Cli),
PrintTokens(print_tokens::Args),
/// Run round-trip source code generation on a given Python file.
RoundTrip(round_trip::Cli),
RoundTrip(round_trip::Args),
}
fn main() -> Result<()> {
let cli = Cli::parse();
match &cli.command {
Commands::GenerateAll(args) => generate_all::main(args)?,
Commands::GenerateJSONSchema(args) => generate_json_schema::main(args)?,
Commands::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Commands::GenerateOptions(args) => generate_options::main(args)?,
Commands::GenerateCliHelp(args) => generate_cli_help::main(args)?,
Commands::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,
Commands::RoundTrip(args) => round_trip::main(args)?,
let args = Args::parse();
match &args.command {
Command::GenerateAll(args) => generate_all::main(args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(args)?,
Command::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Command::GenerateOptions(args) => generate_options::main(args)?,
Command::GenerateCliHelp(args) => generate_cli_help::main(args)?,
Command::PrintAST(args) => print_ast::main(args)?,
Command::PrintCST(args) => print_cst::main(args)?,
Command::PrintTokens(args) => print_tokens::main(args)?,
Command::RoundTrip(args) => round_trip::main(args)?,
}
Ok(())
}

View File

@@ -4,19 +4,18 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use rustpython_parser::parser;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
let python_ast = parser::parse_program(&contents, &args.file.to_string_lossy())?;
println!("{python_ast:#?}");
Ok(())
}

View File

@@ -4,17 +4,16 @@ use std::fs;
use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::Args;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the CST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
match libcst_native::parse_module(&contents, None) {
Ok(python_cst) => {
println!("{python_cst:#?}");

View File

@@ -4,18 +4,17 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use rustpython_parser::lexer;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file for which to generate the AST.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
for (_, tok, _) in lexer::make_tokenizer(&contents).flatten() {
println!("{tok:#?}");
}

View File

@@ -4,18 +4,17 @@ use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::source_code::round_trip;
#[derive(Args)]
pub struct Cli {
#[derive(clap::Args)]
pub struct Args {
/// Python file to round-trip.
#[arg(required = true)]
file: PathBuf,
}
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
println!("{}", round_trip(&contents, &cli.file.to_string_lossy())?);
pub fn main(args: &Args) -> Result<()> {
let contents = fs::read_to_string(&args.file)?;
println!("{}", round_trip(&contents, &args.file.to_string_lossy())?);
Ok(())
}

View File

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

View File

@@ -1,3 +1,4 @@
use std::cmp::Reverse;
use std::collections::HashSet;
use proc_macro2::{Ident, Span};
@@ -14,13 +15,15 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let mut parsed = Vec::new();
let mut prefix_match_arms = quote!();
let mut common_prefix_match_arms = quote!();
let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules",);
let mut url_match_arms = quote!(Self::Ruff => None,);
let mut into_iter_match_arms = quote!();
let mut all_prefixes = HashSet::new();
for variant in variants {
let mut first_chars = HashSet::new();
let prefixes: Result<Vec<_>, _> = variant
.attrs
.iter()
@@ -32,7 +35,9 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let str = lit.value();
match str.chars().next() {
None => return Err(Error::new(lit.span(), "expected prefix string to be non-empty")),
Some(_) => {},
Some(c) => if !first_chars.insert(c) {
return Err(Error::new(lit.span(), format!("this variant already has another prefix starting with the character '{c}'")))
}
}
if !all_prefixes.insert(str.clone()) {
return Err(Error::new(lit.span(), "prefix has already been defined before"));
@@ -62,31 +67,43 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
}
for lit in &prefixes {
parsed.push((lit.clone(), variant_ident.clone()));
parsed.push((
lit.clone(),
variant_ident.clone(),
match prefixes.len() {
1 => ParseStrategy::SinglePrefix,
_ => ParseStrategy::MultiplePrefixes,
},
));
}
prefix_match_arms.extend(quote! {
Self::#variant_ident => &[#(#prefixes),*],
});
if let [prefix] = &prefixes[..] {
common_prefix_match_arms.extend(quote! { Self::#variant_ident => #prefix, });
let prefix_ident = Ident::new(prefix, Span::call_site());
into_iter_match_arms.extend(quote! {
#ident::#variant_ident => RuleCodePrefix::#prefix_ident.into_iter(),
});
} else {
// There is more than one prefix. We already previously asserted
// that prefixes of the same variant don't start with the same character
// so the common prefix for this variant is the empty string.
common_prefix_match_arms.extend(quote! { Self::#variant_ident => "", });
}
}
parsed.sort_by_key(|(prefix, _)| prefix.len());
parsed.sort_by_key(|(prefix, ..)| Reverse(prefix.len()));
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();
for (prefix, field) in parsed {
for (prefix, field, strategy) in parsed {
let ret_str = match strategy {
ParseStrategy::SinglePrefix => quote!(rest),
ParseStrategy::MultiplePrefixes => quote!(code),
};
if_statements.extend(quote! {if let Some(rest) = code.strip_prefix(#prefix) {
return Some((#ident::#field, rest));
return Some((#ident::#field, #ret_str));
}});
let prefix_ident = Ident::new(&prefix, Span::call_site());
if field != "Pycodestyle" {
into_iter_match_arms.extend(quote! {
#ident::#field => RuleCodePrefix::#prefix_ident.into_iter(),
});
}
}
into_iter_match_arms.extend(quote! {
@@ -103,9 +120,8 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
None
}
fn prefixes(&self) -> &'static [&'static str] {
match self { #prefix_match_arms }
fn common_prefix(&self) -> &'static str {
match self { #common_prefix_match_arms }
}
fn name(&self) -> &'static str {
@@ -151,3 +167,8 @@ fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
}
enum ParseStrategy {
SinglePrefix,
MultiplePrefixes,
}

View File

@@ -1,15 +1,4 @@
condense_wildcard_suffixes = true
edition = "2021"
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Lower"
imports_granularity = "Module"
max_width = 100
normalize_comments = true
normalize_doc_attributes = true
reorder_impl_items = true
reorder_imports = true
reorder_modules = true
unstable_features = true
use_field_init_shorthand = true
wrap_comments = true

View File

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

View File

@@ -0,0 +1,67 @@
"""Generate an MkDocs-compatible `docs` and `mkdocs.yml` from the README.md."""
import argparse
import shutil
from pathlib import Path
import yaml
SECTIONS: list[tuple[str, str]] = [
("Overview", "index.md"),
("Installation and Usage", "installation-and-usage.md"),
("Configuration", "configuration.md"),
("Rules", "rules.md"),
("Settings", "settings.md"),
("Editor Integrations", "editor-integrations.md"),
("FAQ", "faq.md"),
]
def main() -> None:
"""Generate an MkDocs-compatible `docs` and `mkdocs.yml`."""
with Path("README.md").open(encoding="utf8") as fp:
content = fp.read()
Path("docs").mkdir(parents=True, exist_ok=True)
# Split the README.md into sections.
for (title, filename) in SECTIONS:
with Path(f"docs/{filename}").open("w+") as f:
block = content.split(f"<!-- Begin section: {title} -->")
if len(block) != 2:
msg = f"Section {title} not found in README.md"
raise ValueError(msg)
block = block[1].split(f"<!-- End section: {title} -->")
if len(block) != 2:
msg = f"Section {title} not found in README.md"
raise ValueError(msg)
f.write(block[0])
# Copy the CONTRIBUTING.md.
shutil.copy("CONTRIBUTING.md", "docs/contributing.md")
# Add the nav section to mkdocs.yml.
with Path("mkdocs.template.yml").open(encoding="utf8") as fp:
config = yaml.safe_load(fp)
config["nav"] = [
{"Overview": "index.md"},
{"Installation and Usage": "installation-and-usage.md"},
{"Configuration": "configuration.md"},
{"Rules": "rules.md"},
{"Settings": "settings.md"},
{"Editor Integrations": "editor-integrations.md"},
{"FAQ": "faq.md"},
{"Contributing": "contributing.md"},
]
with Path("mkdocs.yml").open("w+") as fp:
yaml.safe_dump(config, fp)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate an MkDocs-compatible `docs` and `mkdocs.yml`.",
)
args = parser.parse_args()
main()

View File

@@ -5,6 +5,7 @@ ignore = [
"INP001", # implicit-namespace-package
"PLR2004", # magic-value-comparison
"S101", # assert-used
"EM"
]
[tool.ruff.pydocstyle]

View File

@@ -0,0 +1,71 @@
"""Transform the README.md to support a specific deployment target.
By default, we assume that our README.md will be rendered on GitHub. However, different
targets have different strategies for rendering light- and dark-mode images. This script
adjusts the images in the README.md to support the given target.
"""
import argparse
from pathlib import Path
# https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to
GITHUB = """
<p align="center">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/212613422-7faaf278-706b-4294-ad92-236ffcab3430.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
</picture>
</p>
"""
# https://github.com/pypi/warehouse/issues/11251
PYPI = """
<p align="center">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
</p>
"""
# https://squidfunk.github.io/mkdocs-material/reference/images/#light-and-dark-mode
MK_DOCS = """
<p align="center">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg#only-light">
</p>
<p align="center">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613422-7faaf278-706b-4294-ad92-236ffcab3430.svg#only-dark">
</p>
"""
def main(target: str) -> None:
"""Modify the README.md to support the given target."""
with Path("README.md").open(encoding="utf8") as fp:
content = fp.read()
if GITHUB not in content:
msg = "README.md is not in the expected format."
raise ValueError(msg)
if target == "pypi":
with Path("README.md").open("w", encoding="utf8") as fp:
fp.write(content.replace(GITHUB, PYPI))
elif target == "mkdocs":
with Path("README.md").open("w", encoding="utf8") as fp:
fp.write(content.replace(GITHUB, MK_DOCS))
else:
msg = f"Unknown target: {target}"
raise ValueError(msg)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Modify the README.md to support a specific deployment target.",
)
parser.add_argument(
"--target",
type=str,
required=True,
choices=("pypi", "mkdocs"),
)
args = parser.parse_args()
main(target=args.target)

View File

@@ -489,6 +489,17 @@ pub fn is_const_none(expr: &Expr) -> bool {
)
}
/// Return `true` if an [`Expr`] is `True`.
pub fn is_const_true(expr: &Expr) -> bool {
matches!(
&expr.node,
ExprKind::Constant {
value: Constant::Bool(true),
kind: None
},
)
}
/// Return `true` if a keyword argument is present with a non-`None` value.
pub fn has_non_none_keyword(keywords: &[Keyword], keyword: &str) -> bool {
find_keyword(keywords, keyword).map_or(false, |keyword| {
@@ -548,6 +559,17 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
}
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
has_comments_in(
Range::new(
Location::new(located.location.row(), 0),
Location::new(located.end_location.unwrap().row() + 1, 0),
),
locator,
)
}
/// Returns `true` if a [`Range`] includes at least one comment.
pub fn has_comments_in(range: Range, locator: &Locator) -> bool {
lexer::make_tokenizer(locator.slice_source_code_range(&range))
.any(|result| result.map_or(false, |(_, tok, _)| matches!(tok, Tok::Comment(..))))
@@ -640,6 +662,17 @@ pub fn to_absolute(relative: Location, base: Location) -> Location {
}
}
pub fn to_relative(absolute: Location, base: Location) -> Location {
if absolute.row() == base.row() {
Location::new(
absolute.row() - base.row() + 1,
absolute.column() - base.column(),
)
} else {
Location::new(absolute.row() - base.row() + 1, absolute.column())
}
}
/// Return `true` if a `Stmt` has leading content.
pub fn match_leading_content(stmt: &Stmt, locator: &Locator) -> bool {
let range = Range::new(Location::new(stmt.location.row(), 0), stmt.location);
@@ -665,6 +698,24 @@ pub fn match_trailing_content(stmt: &Stmt, locator: &Locator) -> bool {
false
}
/// If a `Stmt` has a trailing comment, return the index of the hash.
pub fn match_trailing_comment(stmt: &Stmt, locator: &Locator) -> Option<usize> {
let range = Range::new(
stmt.end_location.unwrap(),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
);
let suffix = locator.slice_source_code_range(&range);
for (i, char) in suffix.chars().enumerate() {
if char == '#' {
return Some(i);
}
if !char.is_whitespace() {
return None;
}
}
None
}
/// Return the number of trailing empty lines following a statement.
pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
let suffix =
@@ -974,6 +1025,21 @@ impl<'a> SimpleCallArgs<'a> {
}
}
/// Return `true` if the given `Expr` is a potential logging call. Matches
/// `logging.error`, `logger.error`, `self.logger.error`, etc., but not
/// arbitrary `foo.error` calls.
pub fn is_logger_candidate(func: &Expr) -> bool {
if let ExprKind::Attribute { value, .. } = &func.node {
let call_path = collect_call_path(value);
if let Some(tail) = call_path.last() {
if tail.starts_with("log") || tail.ends_with("logger") || tail.ends_with("logging") {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -1,3 +1,4 @@
use bitflags::bitflags;
use rustc_hash::FxHashMap;
use rustpython_ast::{Cmpop, Located};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
@@ -9,9 +10,21 @@ use crate::ast::types::{Binding, BindingKind, Scope};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
bitflags! {
#[derive(Default)]
pub struct AllNamesFlags: u32 {
const INVALID_FORMAT = 0b0000_0001;
const INVALID_OBJECT = 0b0000_0010;
}
}
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Vec<String> {
fn add_to_names(names: &mut Vec<String>, elts: &[Expr]) {
pub fn extract_all_names(
stmt: &Stmt,
scope: &Scope,
bindings: &[Binding],
) -> (Vec<String>, AllNamesFlags) {
fn add_to_names(names: &mut Vec<String>, elts: &[Expr], flags: &mut AllNamesFlags) {
for elt in elts {
if let ExprKind::Constant {
value: Constant::Str(value),
@@ -19,11 +32,14 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Ve
} = &elt.node
{
names.push(value.to_string());
} else {
*flags |= AllNamesFlags::INVALID_OBJECT;
}
}
}
let mut names: Vec<String> = vec![];
let mut flags = AllNamesFlags::empty();
// Grab the existing bound __all__ values.
if let StmtKind::AugAssign { .. } = &stmt.node {
@@ -42,7 +58,7 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Ve
} {
match &value.node {
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts);
add_to_names(&mut names, elts, &mut flags);
}
ExprKind::BinOp { left, right, .. } => {
let mut current_left = left;
@@ -50,27 +66,40 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Ve
while let Some(elts) = match &current_right.node {
ExprKind::List { elts, .. } => Some(elts),
ExprKind::Tuple { elts, .. } => Some(elts),
_ => None,
_ => {
flags |= AllNamesFlags::INVALID_FORMAT;
None
}
} {
add_to_names(&mut names, elts);
add_to_names(&mut names, elts, &mut flags);
match &current_left.node {
ExprKind::BinOp { left, right, .. } => {
current_left = left;
current_right = right;
}
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
add_to_names(&mut names, elts);
add_to_names(&mut names, elts, &mut flags);
break;
}
_ => {
flags |= AllNamesFlags::INVALID_FORMAT;
break;
}
_ => break,
}
}
}
_ => {}
ExprKind::ListComp { .. } => {
// Allow list comprehensions, even though we can't statically analyze them.
// TODO(charlie): Allow `list()` and `tuple()` calls too, and extract the members
// from them (even if, e.g., it's `list({...})`).
}
_ => {
flags |= AllNamesFlags::INVALID_FORMAT;
}
}
}
names
(names, flags)
}
#[derive(Default)]

View File

@@ -89,9 +89,6 @@ pub struct Scope<'a> {
pub uses_locals: bool,
/// A map from bound name to binding index.
pub values: FxHashMap<&'a str, usize>,
/// A list of (name, index) pairs for bindings that were overridden in the
/// scope.
pub overridden: Vec<(&'a str, usize)>,
}
impl<'a> Scope<'a> {
@@ -102,11 +99,27 @@ impl<'a> Scope<'a> {
import_starred: false,
uses_locals: false,
values: FxHashMap::default(),
overridden: Vec::new(),
}
}
}
// Pyflakes defines the following binding hierarchy (via inheritance):
// Binding
// ExportBinding
// Annotation
// Argument
// Assignment
// NamedExprAssignment
// Definition
// FunctionDefinition
// ClassDefinition
// Builtin
// Importation
// SubmoduleImportation
// ImportationFrom
// StarImportation
// FutureImportation
#[derive(Clone, Debug)]
pub enum BindingKind<'a> {
Annotation,
@@ -127,10 +140,12 @@ pub enum BindingKind<'a> {
SubmoduleImportation(&'a str, &'a str),
}
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub struct Binding<'a> {
pub kind: BindingKind<'a>,
pub range: Range,
/// The context in which the binding was created.
pub context: ExecutionContext,
/// The statement in which the [`Binding`] was defined.
pub source: Option<RefEquality<'a, Stmt>>,
/// Tuple of (scope index, range) indicating the scope and range at which
@@ -146,34 +161,17 @@ pub struct Binding<'a> {
pub synthetic_usage: Option<(usize, Range)>,
}
#[derive(Copy, Clone)]
pub enum UsageContext {
#[derive(Copy, Debug, Clone)]
pub enum ExecutionContext {
Runtime,
Typing,
}
// Pyflakes defines the following binding hierarchy (via inheritance):
// Binding
// ExportBinding
// Annotation
// Argument
// Assignment
// NamedExprAssignment
// Definition
// FunctionDefinition
// ClassDefinition
// Builtin
// Importation
// SubmoduleImportation
// ImportationFrom
// StarImportation
// FutureImportation
impl<'a> Binding<'a> {
pub fn mark_used(&mut self, scope: usize, range: Range, context: UsageContext) {
pub fn mark_used(&mut self, scope: usize, range: Range, context: ExecutionContext) {
match context {
UsageContext::Runtime => self.runtime_usage = Some((scope, range)),
UsageContext::Typing => self.typing_usage = Some((scope, range)),
ExecutionContext::Runtime => self.runtime_usage = Some((scope, range)),
ExecutionContext::Typing => self.typing_usage = Some((scope, range)),
}
}

View File

@@ -1 +0,0 @@

View File

@@ -3,7 +3,9 @@ use itertools::Itertools;
use libcst_native::{
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
};
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::helpers;
use crate::ast::helpers::to_absolute;
@@ -12,7 +14,7 @@ use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::fix::Fix;
use crate::source_code::{Indexer, Locator};
use crate::source_code::{Indexer, Locator, Stylist};
/// Determine if a body contains only a single statement, taking into account
/// deleted.
@@ -157,6 +159,7 @@ pub fn delete_stmt(
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
@@ -179,7 +182,11 @@ pub fn delete_stmt(
} else if helpers::preceded_by_continuation(stmt, indexer) {
if is_end_of_file(stmt, locator) && stmt.location.column() == 0 {
// Special-case: a file can't end in a continuation.
Fix::replacement("\n".to_string(), stmt.location, stmt.end_location.unwrap())
Fix::replacement(
stylist.line_ending().to_string(),
stmt.location,
stmt.end_location.unwrap(),
)
} else {
Fix::deletion(stmt.location, stmt.end_location.unwrap())
}
@@ -200,6 +207,7 @@ pub fn remove_unused_imports<'a>(
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(module_text)?;
@@ -237,7 +245,7 @@ pub fn remove_unused_imports<'a>(
if !found_star {
bail!("Expected \'*\' for unused import");
}
return delete_stmt(stmt, parent, deleted, locator, indexer);
return delete_stmt(stmt, parent, deleted, locator, indexer, stylist);
} else {
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
@@ -298,9 +306,13 @@ pub fn remove_unused_imports<'a>(
}
if aliases.is_empty() {
delete_stmt(stmt, parent, deleted, locator, indexer)
delete_stmt(stmt, parent, deleted, locator, indexer, stylist)
} else {
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
@@ -311,6 +323,109 @@ pub fn remove_unused_imports<'a>(
}
}
/// Generic function to remove arguments or keyword arguments in function
/// calls and class definitions. (For classes `args` should be considered
/// `bases`)
///
/// Supports the removal of parentheses when this is the only (kw)arg left.
/// For this behavior, set `remove_parentheses` to `true`.
pub fn remove_argument(
locator: &Locator,
stmt_at: Location,
expr_at: Location,
expr_end: Location,
args: &[Expr],
keywords: &[Keyword],
remove_parentheses: bool,
) -> Result<Fix> {
// TODO(sbrugman): Preserve trailing comments.
let contents = locator.slice_source_code_at(stmt_at);
let mut fix_start = None;
let mut fix_end = None;
let n_arguments = keywords.len() + args.len();
if n_arguments == 0 {
bail!("No arguments or keywords to remove");
}
if n_arguments == 1 {
// Case 1: there is only one argument.
let mut count: usize = 0;
for (start, tok, end) in lexer::make_tokenizer_located(contents, stmt_at).flatten() {
if matches!(tok, Tok::Lpar) {
if count == 0 {
fix_start = Some(if remove_parentheses {
start
} else {
Location::new(start.row(), start.column() + 1)
});
}
count += 1;
}
if matches!(tok, Tok::Rpar) {
count -= 1;
if count == 0 {
fix_end = Some(if remove_parentheses {
end
} else {
Location::new(end.row(), end.column() - 1)
});
break;
}
}
}
} else if args
.iter()
.map(|node| node.location)
.chain(keywords.iter().map(|node| node.location))
.any(|location| location > expr_at)
{
// Case 2: argument or keyword is _not_ the last node.
let mut seen_comma = false;
for (start, tok, end) in lexer::make_tokenizer_located(contents, stmt_at).flatten() {
if seen_comma {
if matches!(tok, Tok::NonLogicalNewline) {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if matches!(tok, Tok::Newline) {
end
} else {
start
});
break;
}
if start == expr_at {
fix_start = Some(start);
}
if fix_start.is_some() && matches!(tok, Tok::Comma) {
seen_comma = true;
}
}
} else {
// Case 3: argument or keyword is the last node, so we have to find the last
// comma in the stmt.
for (start, tok, _) in lexer::make_tokenizer_located(contents, stmt_at).flatten() {
if start == expr_at {
fix_end = Some(expr_end);
break;
}
if matches!(tok, Tok::Comma) {
fix_start = Some(start);
}
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Fix::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -8,7 +8,6 @@ use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Locator;
pub mod fixer;
pub mod helpers;
/// Auto-fix errors in a file, and write the fixed source code to disk.
@@ -28,7 +27,7 @@ fn apply_fixes<'a>(
fixes: impl Iterator<Item = &'a Fix>,
locator: &'a Locator<'a>,
) -> (String, usize) {
let mut output = String::new();
let mut output = String::with_capacity(locator.len());
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
let mut num_fixed: usize = 0;
@@ -67,11 +66,29 @@ fn apply_fixes<'a>(
(output, num_fixed)
}
/// Apply a single fix.
pub(crate) fn apply_fix(fix: &Fix, locator: &Locator) -> String {
let mut output = String::with_capacity(locator.len());
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range::new(Location::new(1, 0), fix.location));
output.push_str(slice);
// Add the patch itself.
output.push_str(&fix.content);
// Add the remaining content.
let slice = locator.slice_source_code_at(fix.end_location);
output.push_str(slice);
output
}
#[cfg(test)]
mod tests {
use rustpython_parser::ast::Location;
use crate::autofix::apply_fixes;
use crate::autofix::{apply_fix, apply_fixes};
use crate::fix::Fix;
use crate::source_code::Locator;
@@ -85,7 +102,7 @@ mod tests {
}
#[test]
fn apply_single_replacement() {
fn apply_one_replacement() {
let fixes = vec![Fix {
content: "Bar".to_string(),
location: Location::new(1, 8),
@@ -111,7 +128,7 @@ class A(Bar):
}
#[test]
fn apply_single_removal() {
fn apply_one_removal() {
let fixes = vec![Fix {
content: String::new(),
location: Location::new(1, 7),
@@ -137,7 +154,7 @@ class A:
}
#[test]
fn apply_double_removal() {
fn apply_two_removals() {
let fixes = vec![
Fix {
content: String::new(),
@@ -202,4 +219,31 @@ class A:
);
assert_eq!(fixed, 1);
}
#[test]
fn apply_single_fix() {
let locator = Locator::new(
r#"
class A(object):
...
"#
.trim(),
);
let contents = apply_fix(
&Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
},
&locator,
);
assert_eq!(
contents,
r#"
class A:
...
"#
.trim()
);
}
}

View File

@@ -1,5 +1,6 @@
//! Lint rules based on AST traversal.
use std::iter;
use std::path::Path;
use itertools::Itertools;
@@ -16,11 +17,11 @@ use rustpython_parser::parser;
use smallvec::smallvec;
use crate::ast::helpers::{binding_range, collect_call_path, extract_handler_names};
use crate::ast::operations::extract_all_names;
use crate::ast::operations::{extract_all_names, AllNamesFlags};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingKind, CallPath, ClassDef, FunctionDef, Lambda, Node, Range, RefEquality, Scope,
ScopeKind, UsageContext,
Binding, BindingKind, CallPath, ClassDef, ExecutionContext, FunctionDef, Lambda, Node, Range,
RefEquality, Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, visitor};
@@ -34,10 +35,11 @@ use crate::registry::{Diagnostic, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_return, flake8_simplify,
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, flake8_use_pathlib, mccabe,
pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -79,7 +81,7 @@ pub struct Checker<'a> {
pub(crate) exprs: Vec<RefEquality<'a, Expr>>,
pub(crate) scopes: Vec<Scope<'a>>,
pub(crate) scope_stack: Vec<usize>,
pub(crate) dead_scopes: Vec<usize>,
pub(crate) dead_scopes: Vec<(usize, Vec<usize>)>,
deferred_string_type_definitions: Vec<(Range, &'a str, bool, DeferralContext<'a>)>,
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext<'a>)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext<'a>, VisibleScope)>,
@@ -91,6 +93,7 @@ pub struct Checker<'a> {
in_type_definition: bool,
in_deferred_string_type_definition: bool,
in_deferred_type_definition: bool,
in_exception_handler: bool,
in_literal: bool,
in_subscript: bool,
in_type_checking_block: bool,
@@ -100,7 +103,6 @@ pub struct Checker<'a> {
except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
pub(crate) type_checking_blocks: Vec<&'a Stmt>,
}
impl<'a> Checker<'a> {
@@ -152,6 +154,7 @@ impl<'a> Checker<'a> {
in_type_definition: false,
in_deferred_string_type_definition: false,
in_deferred_type_definition: false,
in_exception_handler: false,
in_literal: false,
in_subscript: false,
in_type_checking_block: false,
@@ -161,7 +164,6 @@ impl<'a> Checker<'a> {
except_handlers: vec![],
// Check-specific state.
flake8_bugbear_seen: vec![],
type_checking_blocks: vec![],
}
}
@@ -330,6 +332,7 @@ where
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
// Add the binding to the current scope.
let context = self.execution_context();
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -341,6 +344,7 @@ where
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
context,
});
scope.values.insert(name, index);
}
@@ -357,6 +361,7 @@ where
let scope_index = *self.scope_stack.last().expect("No current scope found");
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
let context = self.execution_context();
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for (name, range) in names.iter().zip(ranges.iter()) {
@@ -369,6 +374,7 @@ where
typing_usage: None,
range: *range,
source: Some(RefEquality(stmt)),
context,
});
scope.values.insert(name, index);
}
@@ -388,7 +394,9 @@ where
if !exists {
if self.settings.rules.enabled(&Rule::NonlocalWithoutBinding) {
self.diagnostics.push(Diagnostic::new(
violations::NonlocalWithoutBinding(name.to_string()),
violations::NonlocalWithoutBinding {
name: name.to_string(),
},
*range,
));
}
@@ -674,6 +682,8 @@ where
for expr in &args.defaults {
self.visit_expr(expr);
}
let context = self.execution_context();
self.add_binding(
name,
Binding {
@@ -683,8 +693,12 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context,
},
);
if self.settings.rules.enabled(&Rule::TooManyArgs) {
pylint::rules::too_many_args(self, args, stmt);
}
}
StmtKind::Return { .. } => {
if self.settings.rules.enabled(&Rule::ReturnOutsideFunction) {
@@ -837,6 +851,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
} else {
@@ -876,6 +891,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -1118,6 +1134,7 @@ where
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -1128,9 +1145,9 @@ where
if self.settings.rules.enabled(&Rule::FutureFeatureNotDefined) {
if !ALL_FEATURE_NAMES.contains(&&*alias.node.name) {
self.diagnostics.push(Diagnostic::new(
violations::FutureFeatureNotDefined(
alias.node.name.to_string(),
),
violations::FutureFeatureNotDefined {
name: alias.node.name.to_string(),
},
Range::from_located(alias),
));
}
@@ -1154,6 +1171,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -1162,12 +1180,12 @@ where
[*(self.scope_stack.last().expect("No current scope found"))];
if !matches!(scope.kind, ScopeKind::Module) {
self.diagnostics.push(Diagnostic::new(
violations::ImportStarNotPermitted(
helpers::format_import_from(
violations::ImportStarNotPermitted {
name: helpers::format_import_from(
level.as_ref(),
module.as_deref(),
),
),
},
Range::from_located(stmt),
));
}
@@ -1175,10 +1193,12 @@ where
if self.settings.rules.enabled(&Rule::ImportStarUsed) {
self.diagnostics.push(Diagnostic::new(
violations::ImportStarUsed(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
)),
violations::ImportStarUsed {
name: helpers::format_import_from(
level.as_ref(),
module.as_deref(),
),
},
Range::from_located(stmt),
));
}
@@ -1228,6 +1248,7 @@ where
typing_usage: None,
range,
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -1394,6 +1415,16 @@ where
pyupgrade::rules::os_error_alias(self, &item);
}
}
if self.settings.rules.enabled(&Rule::RaiseVanillaClass) {
if let Some(expr) = exc {
tryceratops::rules::raise_vanilla_class(self, expr);
}
}
if self.settings.rules.enabled(&Rule::RaiseVanillaArgs) {
if let Some(expr) = exc {
tryceratops::rules::raise_vanilla_args(self, expr);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
@@ -1607,6 +1638,9 @@ where
if self.settings.rules.enabled(&Rule::RaiseWithinTry) {
tryceratops::rules::raise_within_try(self, body);
}
if self.settings.rules.enabled(&Rule::ErrorInsteadOfException) {
tryceratops::rules::error_instead_of_exception(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
@@ -1682,6 +1716,7 @@ where
}
// Recurse.
let prev_in_exception_handler = self.in_exception_handler;
let prev_visible_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef {
@@ -1736,6 +1771,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
context: self.execution_context(),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
@@ -1799,6 +1835,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
context: self.execution_context(),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
@@ -1831,9 +1868,13 @@ where
}
self.visit_body(body);
self.except_handlers.pop();
self.in_exception_handler = true;
for excepthandler in handlers {
self.visit_excepthandler(excepthandler);
}
self.in_exception_handler = prev_in_exception_handler;
self.visit_body(orelse);
self.visit_body(finalbody);
}
@@ -1862,7 +1903,6 @@ where
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
}
self.type_checking_blocks.push(stmt);
let prev_in_type_checking_block = self.in_type_checking_block;
self.in_type_checking_block = true;
@@ -1894,6 +1934,7 @@ where
typing_usage: None,
range: Range::from_located(stmt),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -2058,11 +2099,6 @@ where
{
pyupgrade::rules::use_pep585_annotation(self, expr);
}
if self.settings.rules.enabled(&Rule::RemoveSixCompat) {
pyupgrade::rules::remove_six_compat(self, expr);
}
if self.settings.rules.enabled(&Rule::DatetimeTimezoneUTC)
&& self.settings.target_version >= PythonVersion::Py311
{
@@ -2074,16 +2110,13 @@ where
if self.settings.rules.enabled(&Rule::RewriteMockImport) {
pyupgrade::rules::rewrite_mock_attribute(self, expr);
}
if self.settings.rules.enabled(&Rule::SixPY3Referenced) {
flake8_2020::rules::name_or_attribute(self, expr);
}
pandas_vet::rules::check_attr(self, attr, value, expr);
if self.settings.rules.enabled(&Rule::BannedApi) {
flake8_tidy_imports::banned_api::banned_attribute_access(self, expr);
}
pandas_vet::rules::check_attr(self, attr, value, expr);
}
ExprKind::Call {
func,
@@ -2117,9 +2150,9 @@ where
.enabled(&Rule::StringDotFormatInvalidFormat)
{
self.diagnostics.push(Diagnostic::new(
violations::StringDotFormatInvalidFormat(
pyflakes::format::error_to_string(&e),
),
violations::StringDotFormatInvalidFormat {
message: pyflakes::format::error_to_string(&e),
},
location,
));
}
@@ -2196,9 +2229,6 @@ where
if self.settings.rules.enabled(&Rule::RedundantOpenModes) {
pyupgrade::rules::redundant_open_modes(self, expr);
}
if self.settings.rules.enabled(&Rule::RemoveSixCompat) {
pyupgrade::rules::remove_six_compat(self, expr);
}
if self.settings.rules.enabled(&Rule::NativeLiterals) {
pyupgrade::rules::native_literals(self, expr, func, args, keywords);
}
@@ -2448,8 +2478,9 @@ where
// pandas-vet
if self.settings.rules.enabled(&Rule::UseOfInplaceArgument) {
self.diagnostics
.extend(pandas_vet::rules::inplace_argument(keywords).into_iter());
self.diagnostics.extend(
pandas_vet::rules::inplace_argument(self, expr, args, keywords).into_iter(),
);
}
pandas_vet::rules::check_call(self, func);
@@ -2642,6 +2673,19 @@ where
{
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
// flake8-logging-format
if self.settings.rules.enabled(&Rule::LoggingStringFormat)
|| self.settings.rules.enabled(&Rule::LoggingPercentFormat)
|| self.settings.rules.enabled(&Rule::LoggingStringConcat)
|| self.settings.rules.enabled(&Rule::LoggingFString)
|| self.settings.rules.enabled(&Rule::LoggingWarn)
|| self.settings.rules.enabled(&Rule::LoggingExtraAttrClash)
|| self.settings.rules.enabled(&Rule::LoggingExcInfo)
|| self.settings.rules.enabled(&Rule::LoggingRedundantExcInfo)
{
flake8_logging_format::rules::logging_call(self, func, args, keywords);
}
}
ExprKind::Dict { keys, values } => {
if self
@@ -2665,7 +2709,9 @@ where
let scope = self.current_scope();
if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) {
self.diagnostics.push(Diagnostic::new(
violations::YieldOutsideFunction(DeferralKeyword::Yield),
violations::YieldOutsideFunction {
keyword: DeferralKeyword::Yield,
},
Range::from_located(expr),
));
}
@@ -2676,7 +2722,9 @@ where
let scope = self.current_scope();
if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) {
self.diagnostics.push(Diagnostic::new(
violations::YieldOutsideFunction(DeferralKeyword::YieldFrom),
violations::YieldOutsideFunction {
keyword: DeferralKeyword::YieldFrom,
},
Range::from_located(expr),
));
}
@@ -2687,7 +2735,9 @@ where
let scope = self.current_scope();
if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) {
self.diagnostics.push(Diagnostic::new(
violations::YieldOutsideFunction(DeferralKeyword::Await),
violations::YieldOutsideFunction {
keyword: DeferralKeyword::Await,
},
Range::from_located(expr),
));
}
@@ -2773,7 +2823,9 @@ where
.enabled(&Rule::PercentFormatUnsupportedFormatCharacter)
{
self.diagnostics.push(Diagnostic::new(
violations::PercentFormatUnsupportedFormatCharacter(c),
violations::PercentFormatUnsupportedFormatCharacter {
char: c,
},
location,
));
}
@@ -2785,7 +2837,9 @@ where
.enabled(&Rule::PercentFormatInvalidFormat)
{
self.diagnostics.push(Diagnostic::new(
violations::PercentFormatInvalidFormat(e.to_string()),
violations::PercentFormatInvalidFormat {
message: e.to_string(),
},
location,
));
}
@@ -3407,6 +3461,15 @@ where
body,
);
}
if self.settings.rules.enabled(&Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
self,
type_.as_deref(),
name.as_deref(),
body,
self.settings.flake8_bandit.check_typed_exception,
);
}
if self.settings.rules.enabled(&Rule::ReraiseNoCause) {
tryceratops::rules::reraise_no_cause(self, body);
}
@@ -3464,7 +3527,9 @@ where
if !self.bindings[*index].used() {
if self.settings.rules.enabled(&Rule::UnusedVariable) {
let mut diagnostic = Diagnostic::new(
violations::UnusedVariable(name.to_string()),
violations::UnusedVariable {
name: name.to_string(),
},
name_range,
);
if self.patch(&Rule::UnusedVariable) {
@@ -3584,6 +3649,7 @@ where
typing_usage: None,
range: Range::from_located(arg),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
@@ -3660,11 +3726,12 @@ impl<'a> Checker<'a> {
}
fn pop_scope(&mut self) {
self.dead_scopes.push(
self.dead_scopes.push((
self.scope_stack
.pop()
.expect("Attempted to pop without scope"),
);
self.scope_stack.clone(),
));
}
fn bind_builtins(&mut self) {
@@ -3681,9 +3748,10 @@ impl<'a> Checker<'a> {
kind: BindingKind::Builtin,
range: Range::default(),
runtime_usage: None,
synthetic_usage: None,
synthetic_usage: Some((0, Range::default())),
typing_usage: None,
source: None,
context: ExecutionContext::Runtime,
});
scope.values.insert(builtin, index);
}
@@ -3720,6 +3788,22 @@ impl<'a> Checker<'a> {
.map(|index| &self.scopes[*index])
}
pub fn in_exception_handler(&self) -> bool {
self.in_exception_handler
}
pub fn execution_context(&self) -> ExecutionContext {
if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
ExecutionContext::Typing
} else {
ExecutionContext::Runtime
}
}
fn add_binding<'b>(&mut self, name: &'b str, binding: Binding<'a>)
where
'b: 'a,
@@ -3759,10 +3843,10 @@ impl<'a> Checker<'a> {
if matches!(binding.kind, BindingKind::LoopVar) && existing_is_import {
if self.settings.rules.enabled(&Rule::ImportShadowedByLoopVar) {
self.diagnostics.push(Diagnostic::new(
violations::ImportShadowedByLoopVar(
name.to_string(),
existing.range.location.row(),
),
violations::ImportShadowedByLoopVar {
name: name.to_string(),
line: existing.range.location.row(),
},
binding.range,
));
}
@@ -3778,10 +3862,10 @@ impl<'a> Checker<'a> {
{
if self.settings.rules.enabled(&Rule::RedefinedWhileUnused) {
self.diagnostics.push(Diagnostic::new(
violations::RedefinedWhileUnused(
name.to_string(),
existing.range.location.row(),
),
violations::RedefinedWhileUnused {
name: name.to_string(),
line: existing.range.location.row(),
},
binding_range(&binding, self.locator),
));
}
@@ -3795,16 +3879,41 @@ impl<'a> Checker<'a> {
}
}
// Assume the rebound name is used as a global or within a loop.
let scope = self.current_scope();
let binding = match scope.values.get(&name) {
None => binding,
Some(index) => Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
..binding
},
let binding = if let Some(index) = scope.values.get(&name) {
if matches!(self.bindings[*index].kind, BindingKind::Builtin) {
// Avoid overriding builtins.
binding
} else if matches!(self.bindings[*index].kind, BindingKind::Global) {
// If the original binding was a global, and the new binding conflicts within the
// current scope, then the new binding is also a global.
Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
kind: BindingKind::Global,
..binding
}
} else if matches!(self.bindings[*index].kind, BindingKind::Nonlocal) {
// If the original binding was a nonlocal, and the new binding conflicts within the
// current scope, then the new binding is also a nonlocal.
Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
kind: BindingKind::Nonlocal,
..binding
}
} else {
Binding {
runtime_usage: self.bindings[*index].runtime_usage,
synthetic_usage: self.bindings[*index].synthetic_usage,
typing_usage: self.bindings[*index].typing_usage,
..binding
}
}
} else {
binding
};
// Don't treat annotations as assignments if there is an existing value
@@ -3837,17 +3946,8 @@ impl<'a> Checker<'a> {
}
if let Some(index) = scope.values.get(&id.as_str()) {
let context = if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
UsageContext::Typing
} else {
UsageContext::Runtime
};
// Mark the binding as used.
let context = self.execution_context();
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
@@ -3929,7 +4029,10 @@ impl<'a> Checker<'a> {
from_list.sort();
self.diagnostics.push(Diagnostic::new(
violations::ImportStarUsage(id.to_string(), from_list),
violations::ImportStarUsage {
name: id.to_string(),
sources: from_list,
},
Range::from_located(expr),
));
}
@@ -3960,7 +4063,7 @@ impl<'a> Checker<'a> {
}
self.diagnostics.push(Diagnostic::new(
violations::UndefinedName(id.clone()),
violations::UndefinedName { name: id.clone() },
Range::from_located(expr),
));
}
@@ -4030,6 +4133,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4049,6 +4153,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4064,6 +4169,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4103,19 +4209,31 @@ impl<'a> Checker<'a> {
}
_ => false,
} {
let (all_names, all_names_flags) =
extract_all_names(parent, current, &self.bindings);
if self.settings.rules.enabled(&Rule::InvalidAllFormat)
&& matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT)
{
pylint::rules::invalid_all_format(self, expr);
}
if self.settings.rules.enabled(&Rule::InvalidAllObject)
&& matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT)
{
pylint::rules::invalid_all_object(self, expr);
}
self.add_binding(
id,
Binding {
kind: BindingKind::Export(extract_all_names(
parent,
current,
&self.bindings,
)),
kind: BindingKind::Export(all_names),
runtime_usage: None,
synthetic_usage: None,
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
return;
@@ -4131,6 +4249,7 @@ impl<'a> Checker<'a> {
typing_usage: None,
range: Range::from_located(expr),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
}
@@ -4152,7 +4271,9 @@ impl<'a> Checker<'a> {
&& self.settings.rules.enabled(&Rule::UndefinedName)
{
self.diagnostics.push(Diagnostic::new(
violations::UndefinedName(id.to_string()),
violations::UndefinedName {
name: id.to_string(),
},
Range::from_located(expr),
));
}
@@ -4218,7 +4339,9 @@ impl<'a> Checker<'a> {
.enabled(&Rule::ForwardAnnotationSyntaxError)
{
self.diagnostics.push(Diagnostic::new(
violations::ForwardAnnotationSyntaxError(expression.to_string()),
violations::ForwardAnnotationSyntaxError {
body: expression.to_string(),
},
range,
));
}
@@ -4336,13 +4459,48 @@ impl<'a> Checker<'a> {
return;
}
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope in self
.dead_scopes
.iter()
.rev()
.map(|index| &self.scopes[*index])
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let runtime_imports: Vec<Vec<&Binding>> = if !self.settings.flake8_type_checking.strict
&& (self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyFirstPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyThirdPartyImport)
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
self.scopes
.iter()
.map(|scope| {
scope
.values
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for (index, stack) in self.dead_scopes.iter().rev() {
let scope = &self.scopes[*index];
// PLW0602
if self
.settings
@@ -4352,10 +4510,16 @@ impl<'a> Checker<'a> {
for (name, index) in &scope.values {
let binding = &self.bindings[*index];
if matches!(binding.kind, BindingKind::Global) {
diagnostics.push(Diagnostic::new(
violations::GlobalVariableNotAssigned((*name).to_string()),
binding.range,
));
if let Some(stmt) = &binding.source {
if matches!(stmt.node, StmtKind::Global { .. }) {
diagnostics.push(Diagnostic::new(
violations::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range,
));
}
}
}
}
}
@@ -4382,7 +4546,9 @@ impl<'a> Checker<'a> {
for &name in names {
if !scope.values.contains_key(name) {
diagnostics.push(Diagnostic::new(
violations::UndefinedExport(name.to_string()),
violations::UndefinedExport {
name: name.to_string(),
},
all_binding.range,
));
}
@@ -4420,10 +4586,10 @@ impl<'a> Checker<'a> {
if let Some(indices) = self.redefinitions.get(index) {
for index in indices {
diagnostics.push(Diagnostic::new(
violations::RedefinedWhileUnused(
(*name).to_string(),
binding.range.location.row(),
),
violations::RedefinedWhileUnused {
name: (*name).to_string(),
line: binding.range.location.row(),
},
binding_range(&self.bindings[*index], self.locator),
));
}
@@ -4451,10 +4617,10 @@ impl<'a> Checker<'a> {
for &name in names {
if !scope.values.contains_key(name) {
diagnostics.push(Diagnostic::new(
violations::ImportStarUsage(
name.to_string(),
from_list.clone(),
),
violations::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
all_binding.range,
));
}
@@ -4481,25 +4647,28 @@ impl<'a> Checker<'a> {
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
for (.., index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict {
vec![]
} else {
stack
.iter()
.chain(iter::once(index))
.flat_map(|index| runtime_imports[*index].iter())
.copied()
.collect()
};
for (.., index) in &scope.values {
let binding = &self.bindings[*index];
if let Some(diagnostic) =
flake8_type_checking::rules::runtime_import_in_type_checking_block(
binding,
&self.type_checking_blocks,
)
flake8_type_checking::rules::runtime_import_in_type_checking_block(binding)
{
diagnostics.push(diagnostic);
}
if let Some(diagnostic) =
flake8_type_checking::rules::typing_only_runtime_import(
binding,
&self.type_checking_blocks,
&runtime_imports,
self.package,
self.settings,
)
@@ -4522,11 +4691,7 @@ impl<'a> Checker<'a> {
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in scope
.values
.iter()
.chain(scope.overridden.iter().map(|(a, b)| (a, b)))
{
for (name, index) in &scope.values {
let binding = &self.bindings[*index];
let full_name = match &binding.kind {
@@ -4598,6 +4763,7 @@ impl<'a> Checker<'a> {
&deleted,
self.locator,
self.indexer,
self.stylist,
) {
Ok(fix) => {
if fix.content.is_empty() || fix.content == "pass" {
@@ -4617,7 +4783,11 @@ impl<'a> Checker<'a> {
let multiple = unused_imports.len() > 1;
for (full_name, range) in unused_imports {
let mut diagnostic = Diagnostic::new(
violations::UnusedImport(full_name.to_string(), ignore_init, multiple),
violations::UnusedImport {
name: full_name.to_string(),
ignore_init,
multiple,
},
*range,
);
if matches!(child.node, StmtKind::ImportFrom { .. })
@@ -4639,7 +4809,11 @@ impl<'a> Checker<'a> {
let multiple = unused_imports.len() > 1;
for (full_name, range) in unused_imports {
let mut diagnostic = Diagnostic::new(
violations::UnusedImport(full_name.to_string(), ignore_init, multiple),
violations::UnusedImport {
name: full_name.to_string(),
ignore_init,
multiple,
},
*range,
);
if matches!(child.node, StmtKind::ImportFrom { .. })

View File

@@ -103,7 +103,7 @@ pub fn check_noqa(
Directive::All(spaces, start, end) => {
if matches.is_empty() {
let mut diagnostic = Diagnostic::new(
violations::UnusedNOQA(None),
violations::UnusedNOQA { codes: None },
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)
@@ -154,20 +154,22 @@ pub fn check_noqa(
&& unmatched_codes.is_empty())
{
let mut diagnostic = Diagnostic::new(
violations::UnusedNOQA(Some(UnusedCodes {
disabled: disabled_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
unknown: unknown_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
unmatched: unmatched_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
})),
violations::UnusedNOQA {
codes: Some(UnusedCodes {
disabled: disabled_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
unknown: unknown_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
unmatched: unmatched_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
}),
},
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)

View File

@@ -36,6 +36,8 @@ impl Fix {
}
pub fn replacement(content: String, start: Location, end: Location) -> Self {
debug_assert!(!content.is_empty(), "Prefer `Fix::deletion`");
Self {
content,
location: start,
@@ -44,6 +46,8 @@ impl Fix {
}
pub fn insertion(content: String, at: Location) -> Self {
debug_assert!(!content.is_empty(), "Insert content is empty");
Self {
content,
location: at,

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