Compare commits

...

68 Commits

Author SHA1 Message Date
Charlie Marsh
651f6b6bce Bump Ruff version to 0.0.240 2023-02-02 12:45:23 -05:00
Charlie Marsh
d3c3198b24 Fix versions in BREAKING_CHANGES.md 2023-02-02 12:45:20 -05:00
Charlie Marsh
ec6054edce Treat if 0: and if False: as type-checking blocks (#2485) 2023-02-02 12:35:59 -05:00
Charlie Marsh
a0df78cb7d Visit NamedExpr values before targets (#2484) 2023-02-02 12:21:58 -05:00
Aarni Koskela
ac41c33d1f Move flake8-blind-except violation to rule module (#2479) 2023-02-02 12:21:25 -05:00
Aarni Koskela
b4b8782243 Move remaining flake8-pytest-style violations to rule modules (#2482) 2023-02-02 12:10:49 -05:00
Florian Best
8e53a4d1d3 fix: assertTrue()/assertFalse() fixer should not test for identity (#2476) 2023-02-02 11:24:35 -05:00
Charlie Marsh
668860cba3 Add more information to Pylint FAQ section 2023-02-02 11:08:17 -05:00
Aarni Koskela
038e8cfba0 Move flake8-quotes violations to rules module (#2475) 2023-02-02 10:08:12 -05:00
Aarni Koskela
ebfa55cea3 Move flake8-builtins violations to rules file (#2478) 2023-02-02 10:03:09 -05:00
Aarni Koskela
aa0fc0f9c2 Move flake8-bugbear violations to rule modules (#2474) 2023-02-02 09:34:26 -05:00
Aarni Koskela
aa85c81280 Move flake8-comprehensions violations to rule files (#2477) 2023-02-02 09:26:50 -05:00
Charlie Marsh
f5fd6f59ea Remove extraneous test file 2023-02-02 08:46:03 -05:00
Martin Fischer
540e31f5f4 Carry-over ignore to next config layer if select = [] (#2467)
Resolves #2461.
2023-02-02 08:45:07 -05:00
Chris Chan
8136cc9238 Implement pylint's too-many-statements rule (PLR0915) (#2445) 2023-02-02 08:18:37 -05:00
Charlie Marsh
2c71535016 Update snapshots 2023-02-02 08:15:33 -05:00
Aarni Koskela
cce8fb9882 isort: support forced_separate (#2268) 2023-02-02 08:08:02 -05:00
Maksudul Haque
9e59c99133 [flake8-self] Add Plugin and Rule SLF001 (#2470) 2023-02-02 07:58:14 -05:00
Colin Delahunty
b032f50775 [pyupgrade]: Remove outdated sys.version_info blocks (#2099) 2023-02-02 07:49:24 -05:00
Charlie Marsh
1c2fc38853 Use LibCST to reverse Yoda conditions (#2468)
Our existing solution was having trouble with parenthesized expressions. This actually may affect more than `SIM300`, but let's address them as they come up.

Closes #2466.
2023-02-02 00:07:43 -05:00
Charlie Marsh
f16f3a4a03 Avoid removing un-selected codes when applying --add-noqa edits (#2465)
The downside here is that we have to leave blank `# noqa` directives intact. Otherwise, we risk removing necessary `# noqa` coverage for rules that aren't selected.

Closes #2254.
2023-02-01 22:22:31 -05:00
Charlie Marsh
30a09ec211 Respect parent noqa in --add-noqa (#2464) 2023-02-01 21:58:01 -05:00
Reid Swan
ec7b25290b feat: Add isort option lines-after-imports (#2440)
Fixes https://github.com/charliermarsh/ruff/issues/2243

Adds support for the isort option [lines_after_imports](https://pycqa.github.io/isort/docs/configuration/options.html#lines-after-imports) to insert blank lines between imports and the follow up code.
2023-02-01 21:39:45 -05:00
Charlie Marsh
68422d4ff2 Allow non-ruff.toml-named files for --config (#2463)
Previously, if you passed in a file on the command-line via `--config`, it had to be named either `pyproject.toml` or `ruff.toml` -- otherwise, we errored. I think this is too strict. `pyproject.toml` is a special name in the ecosystem, so we should require _that_; but otherwise, let's just assume it's in `ruff.toml` format.

As an alternative, we could add a `--pyproject` argument for `pyproject.toml`, and assume anything passed to `--config` is in `ruff.toml` format. But that _would_ be a breaking change and is arguably more confusing. (This isn't a breaking change, since it only loosens the CLI.)

Closes #2462.
2023-02-01 21:35:42 -05:00
Charlie Marsh
2abaffd65b Improve consistency of backticks for plugin names (#2460) 2023-02-01 19:17:32 -05:00
Charlie Marsh
06cbf5a2ae Add some top-level links to the README (#2458) 2023-02-01 19:10:41 -05:00
Charlie Marsh
f432ce291a Add Fathom to docs 2023-02-01 18:41:24 -05:00
Charlie Marsh
1eb331143d Add Fathom to playground 2023-02-01 18:30:40 -05:00
Henry Schreiner
db1b1672b8 fix: minor spacing typo in message for PTH123 (#2453) 2023-02-01 14:39:50 -05:00
Charlie Marsh
6861e59103 Only avoid PEP604 rewrites for pre-Python 3.10 code (#2449)
I moved the `self.in_annotation` guard out of the version check in #1563. But, I think that was a mistake. It was done to resolve #1560, but the fix in that case _should've_ been to set a different Python version.

Closes #2447.
2023-02-01 13:03:51 -05:00
Charlie Marsh
778c644ee3 Trigger, but don't fix, SIM rules if comments are present (#2450) 2023-02-01 12:56:02 -05:00
Martin Fischer
e66a6b6d05 refactor: Define ruff_dev::ROOT_DIR 2023-02-01 09:17:53 -05:00
Martin Fischer
faea478ca5 fix: failing snapshot test on Windows 2023-02-01 09:17:53 -05:00
Martin Fischer
39b5fa0e24 refactor: Make test_path prefix the fixture path 2023-02-01 09:17:53 -05:00
Martin Fischer
df413d1ece refactor: Introduce test_resource_path helper 2023-02-01 09:17:53 -05:00
Martin Fischer
cfd0693ae5 refactor: Document internal test_path function 2023-02-01 09:17:53 -05:00
Martin Fischer
56ad160c05 refactor: Move test_path helper to new test module 2023-02-01 09:17:53 -05:00
Florian Best
9d8c6ba671 more builtin name checks when autofixing (#2430) 2023-02-01 08:16:47 -05:00
Charlie Marsh
1ea88ea56b Avoid iterating over body twice (#2439) 2023-02-01 08:12:36 -05:00
Florian Best
7f44ffb55c docs(CONTRIBUTING): add instructions how to update the test snapshots (#2412) 2023-02-01 07:44:20 -05:00
Charlie Marsh
dbd640d90f Remove unused Cargo.lock file (#2437) 2023-02-01 07:33:59 -05:00
Aarni Koskela
e5082c7d6c isort: split up package (#2434) 2023-02-01 07:17:31 -05:00
Charlie Marsh
841d176289 Move super-args and unnecessary-coding-comment into their own modules (#2432) 2023-01-31 22:26:56 -05:00
Charlie Marsh
c15595325c Bump version to 0.0.239 2023-01-31 19:06:22 -05:00
Florian Best
e97b1a4280 fix: ignore fix if "bool" is not builtin (#2429) 2023-01-31 19:03:46 -05:00
Florian Best
82ec884a61 feat: let SIM210 return expressions without bool() wrapping (#2410) (#2426) 2023-01-31 18:25:22 -05:00
Maksudul Haque
7c1a6bce7b [flake8-raise] Add Plugin and RSE102 Rule (#2354) 2023-01-31 18:09:40 -05:00
Charlie Marsh
84a8b628b8 Avoid implicit-namespace-package checks for .pyi files (#2420) 2023-01-31 17:35:30 -05:00
Charlie Marsh
142b627bb8 Avoid Bandit false-positives for empty-string-as-password (#2421) 2023-01-31 16:56:03 -05:00
Charlie Marsh
fbf231e1b8 Allow implicit multiline strings with internal quotes to use non-preferred quote (#2416)
As an example, if you have `single` as your preferred style, we'll now allow this:

```py
assert s.to_python(123) == (
    "123 info=SerializationInfo(include=None, exclude=None, mode='python', by_alias=True, exclude_unset=False, "
    "exclude_defaults=False, exclude_none=False, round_trip=False)"
)
```

Previously, the second line of the implicit string concatenation would be flagged as invalid, despite the _first_ line requiring double quotes. (Note that we'll accept either single or double quotes for that second line.)

Mechanically, this required that we process sequences of `Tok::String` rather than a single `Tok::String` at a time. Prior to iterating over the strings in the sequence, we check if any of them require the non-preferred quote style; if so, we let _any_ of them use it.

Closes #2400.
2023-01-31 16:27:15 -05:00
Florian Best
1dd9ccf7f6 feat: let SIM103 return expressions without bool() wrapping (#2410) 2023-01-31 16:11:44 -05:00
Charlie Marsh
d601abe01b Rename flake8-quotes snapshots and tests (#2415) 2023-01-31 16:08:00 -05:00
Charlie Marsh
15d4774b6b Avoid flagging same-condition cases in SIM103 (#2404) 2023-01-31 12:45:51 -05:00
Charlie Marsh
293c7e00d5 Include method name in B027 message (#2403) 2023-01-31 12:41:22 -05:00
Thomas M Kehrenberg
c3a3195922 Fix option name "max-args" in the documentation (#2401) 2023-01-31 12:30:05 -05:00
Martin Fischer
39d98d3488 Disable panic hook about reporting issues for debug builds
In order to avoid confusing new developers.  When a debug build panics
chances are that the panic is caused by local changes and should in
fact not be reported on GitHub.
2023-01-31 12:24:26 -05:00
Charlie Marsh
cd3d82213a Handle multi-byte lines in RUF100 (#2392) 2023-01-31 07:59:16 -05:00
Charlie Marsh
a9a0026f2f Don't panic for --statistics with no errors (#2391) 2023-01-31 07:53:29 -05:00
Hassan Kibirige
da4618d77b For neovim:null_ls use ruff builtin for formatting (#2386)
null_ls picked up the recommended snippet in README.md and ruff formatting now a builtin.

Ref:
1. 482990e391

2. 7b2b28e207/doc/BUILTINS.md (ruff-1)
2023-01-31 07:22:14 -05:00
Martin Fischer
1b0748d19d refactor: Simplify Linter::categories 2023-01-31 07:21:12 -05:00
Martin Fischer
0b7fa64481 refactor: Drop PartialOrd & Ord impls for RuleSelector
RuleSelector implemented PartialOrd & Ord because ruff::flake8_to_ruff
was using RuleSelector within a BTreeSet (which requires contained
elements to implement Ord). There however is no inherent order to
rule selectors, so PartialOrd & Ord should not be implemented.

This commit changes BTreeSet<RuleSelector> to HashSet<RuleSelector>
and adds an explicit sort calls based on the serialized strings,
letting us drop the PartialOrd & Ord impls in favor of a Hash impl.
2023-01-31 07:21:12 -05:00
Samuel Cormier-Iijima
09d593b124 [I001] fix isort check for files with tabs and no indented blocks (#2374)
This is a followup to #2361. The isort check still had an issue in a rather specific case: files with a multiline import, indented with tabs, and not containing any indented blocks.

The root cause is this: [`Stylist`'s indentation detection](ad8693e3de/src/source_code/stylist.rs (L163-L172)) works by finding `Indent` tokens to determine the type of indentation used by a file. This works for indented code blocks (loops/classes/functions/etc) but does not work for multiline values, so falls back to 4 spaces if the file doesn't contain code blocks.

I considered a few possible solutions:

1. Fix `detect_indentation` to avoid tokenizing and instead use some other heuristic to determine indentation. This would have the benefit of working in other places where this is potentially an issue, but would still fail if the file doesn't contain any indentation at all, and would need to fall back to option 2 anyways.
2. Add an option for specifying the default indentation in Ruff's config. I think this would confusing, since it wouldn't affect the detection behavior and only operate as a fallback, has no other current application and would probably end up being overloaded for other things.
3. Relax the isort check by comparing the expected and actual code's lexed tokens. This would require an additional lexing step.
4. Relax the isort check by comparing the expected and actual code modulo whitespace at the start of lines.

This PR does approach 4, which in addition to being the simplest option, has the (expected, although I didn't benchmark) added benefit of improved performance, since the check no longer needs to do two allocations for the two `dedent` calls. I also believe that the check is still correct enough for all practical purposes.
2023-01-31 07:18:54 -05:00
Erik Welch
adc134ced0 Fix typos: s/scripy/scipy/g (#2380) 2023-01-31 07:17:18 -05:00
Charlie Marsh
6051a0c1c8 Include per-file ignore matches in debug logging (#2376) 2023-01-30 23:11:56 -05:00
Charlie Marsh
00495e8620 Use human-readable types for documentation values (#2375) 2023-01-30 23:05:28 -05:00
Colin Delahunty
ad8693e3de [pyupgrade] Implement import-replacement rule (UP035) (#2049) 2023-01-30 19:58:28 -05:00
Charlie Marsh
69e20c4554 Minor improvements to the docs (#2371) 2023-01-30 19:06:05 -05:00
Charlie Marsh
b5816634b3 Add a link to MkDocs (#2370) 2023-01-30 19:00:57 -05:00
292 changed files with 9958 additions and 6927 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
resources/test/cpython
docs/
mkdocs.yml
.overrides
###
# Rust.gitignore

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.238
rev: v0.0.240
hooks:
- id: ruff
args: [--fix]

View File

@@ -37,7 +37,7 @@ will enable all `F` rules, including `F401`, as the command line's `--select` re
The `remove-six-compat` rule has been removed. This rule was only useful for one-time Python 2-to-3
upgrades.
## 0.0.238
## 0.0.237
### `--explain`, `--clean`, and `--generate-shell-completion` are now subcommands ([#2190](https://github.com/charliermarsh/ruff/pull/2190))

View File

@@ -4,10 +4,14 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
## The basics
Ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration
options), consider submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
your proposed change.
Ruff welcomes contributions in the form of Pull Requests.
For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/charliermarsh/ruff/issues) outlining your proposed
change. You can also join us on [**Discord**](https://discord.gg/Z8KbeK24) to discuss your idea with
the community.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
@@ -50,7 +54,14 @@ 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
Note that many code changes also require updating the snapshot tests, which is done interactively
after running `cargo test` like so:
```shell
cargo insta review
```
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

10
Cargo.lock generated
View File

@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.238"
version = "0.0.240"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -1922,7 +1922,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.238"
version = "0.0.240"
dependencies = [
"anyhow",
"bitflags",
@@ -1977,7 +1977,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.238"
version = "0.0.240"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2014,7 +2014,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.238"
version = "0.0.240"
dependencies = [
"anyhow",
"clap 4.1.4",
@@ -2035,7 +2035,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.238"
version = "0.0.240"
dependencies = [
"once_cell",
"proc-macro2",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.238"
version = "0.0.240"
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.238", path = "ruff_macros" }
ruff_macros = { version = "0.0.240", 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" }
@@ -77,7 +77,7 @@ wasm-bindgen = { version = "0.2.83" }
is_executable = "1.0.1"
[dev-dependencies]
insta = { version = "1.19.1", features = ["yaml", "redactions"] }
insta = { version = "1.19.0", features = ["yaml", "redactions"] }
test-case = { version = "2.2.2" }
wasm-bindgen-test = { version = "0.3.33" }

30
LICENSE
View File

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

504
README.md

File diff suppressed because it is too large Load Diff

2964
flake8_to_ruff/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -23,6 +23,7 @@ theme:
toggle:
icon: material/weather-night
name: Switch to light mode
custom_dir: .overrides
repo_url: https://github.com/charliermarsh/ruff
repo_name: ruff
site_author: charliermarsh

View File

@@ -14,6 +14,7 @@
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>"
/>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<script src="https://cdn.usefathom.com/script.js" data-site="XWUDIXNB" defer></script>
</head>
<body>
<div id="root"></div>

View File

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

View File

@@ -4,6 +4,7 @@ d = {}
safe = "s3cr3t"
password = True
password = safe
password = ""
password is True
password == 1
d["safe"] = "s3cr3t"

View File

@@ -7,6 +7,7 @@ string = "Hello World"
# OK
func("s3cr3t")
func(1, password=string)
func(1, password="")
func(pos="s3cr3t", password=string)
# Error

View File

@@ -28,3 +28,7 @@ def ok_all(first, /, pos, default="posonly", *, kwonly="kwonly"):
def default_all(first, /, pos, secret="posonly", *, password="kwonly"):
pass
def ok_empty(first, password=""):
pass

View File

@@ -1,2 +1,4 @@
this_should_be_linted = "double quote string"
this_should_be_linted = u"double quote string"
this_should_be_linted = f"double quote string"
this_should_be_linted = f"double {'quote'} string"

View File

@@ -4,3 +4,8 @@ this_is_fine = '"This" is a \'string\''
this_is_fine = "This is a 'string'"
this_is_fine = "\"This\" is a 'string'"
this_is_fine = r'This is a \'string\''
this_is_fine = R'This is a \'string\''
this_should_raise = (
'This is a'
'\'string\''
)

View File

@@ -0,0 +1,27 @@
x = (
"This"
"is"
"not"
)
x = (
"This" \
"is" \
"not"
)
x = (
"This"
"is 'actually'"
"fine"
)
x = (
"This" \
"is 'actually'" \
"fine"
)
if True:
"This can use 'double' quotes"
"But this needs to be changed"

View File

@@ -1,2 +1,4 @@
this_should_be_linted = 'single quote string'
this_should_be_linted = u'double quote string'
this_should_be_linted = f'double quote string'
this_should_be_linted = f'double {"quote"} string'

View File

@@ -3,3 +3,8 @@ this_is_fine = "'This' is a \"string\""
this_is_fine = 'This is a "string"'
this_is_fine = '\'This\' is a "string"'
this_is_fine = r"This is a \"string\""
this_is_fine = R"This is a \"string\""
this_should_raise = (
"This is a"
"\"string\""
)

View File

@@ -0,0 +1,27 @@
x = (
'This'
'is'
'not'
)
x = (
'This' \
'is' \
'not'
)
x = (
'This'
'is "actually"'
'fine'
)
x = (
'This' \
'is "actually"' \
'fine'
)
if True:
'This can use "single" quotes'
'But this needs to be changed'

View File

@@ -0,0 +1,15 @@
try:
y = 6 + "7"
except TypeError:
raise ValueError() # RSE102
try:
x = 1 / 0
except ZeroDivisionError:
raise
raise TypeError() # RSE102
raise AssertionError
raise AttributeError("test message")

View File

@@ -0,0 +1,31 @@
class Foo:
def __init__(self):
self.public_thing = "foo"
self._private_thing = "bar"
self.__really_private_thing = "baz"
def __str__(self):
return "foo"
def public_func(self):
pass
def _private_func(self):
pass
def __really_private_func(self, arg):
pass
foo = Foo()
print(foo.public_thing)
print(foo.public_func())
print(foo.__dict__)
print(foo.__str__())
print(foo._private_thing) # SLF001
print(foo.__really_private_thing) # SLF001
print(foo._private_func()) # SLF001
print(foo.__really_private_func(1)) # SLF001

View File

@@ -21,3 +21,10 @@ if isinstance(a, int) and isinstance(b, bool) or isinstance(a, float):
if isinstance(a, bool) or isinstance(b, str):
pass
def f():
# OK
def isinstance(a, b):
return False
if isinstance(a, int) or isinstance(a, float):
pass

View File

@@ -6,6 +6,14 @@ def f():
return False
def f():
# SIM103
if a == b:
return True
else:
return False
def f():
# SIM103
if a:
@@ -50,3 +58,29 @@ def f():
return False
else:
return True
def f():
# OK
if a:
return False
else:
return False
def f():
# OK
if a:
return True
else:
return True
def f():
# OK
def bool():
return False
if a:
return True
else:
return False

View File

@@ -54,7 +54,7 @@ else:
randbytes = _get_random_bytes
# OK (includes comments)
# SIM108 (without fix due to comments)
if x > 0:
# test test
abc = x
@@ -93,8 +93,20 @@ if True:
b = ddddddddddddddddddddddddddddddddddddd
# OK (trailing comments)
# SIM108 (without fix due to trailing comment)
if True:
exitcode = 0
else:
exitcode = 1 # Trailing comment
# SIM108
if True: x = 3 # Foo
else: x = 5
# SIM108
if True: # Foo
x = 3
else:
x = 5

View File

@@ -115,3 +115,43 @@ def f():
else:
return True
return False
def f():
def any(exp):
pass
for x in iterable:
if check(x):
return True
return False
def f():
def all(exp):
pass
for x in iterable:
if check(x):
return False
return True
def f():
x = 1
# SIM110
for x in iterable:
if check(x):
return True
return False
def f():
x = 1
# SIM111
for x in iterable:
if check(x):
return False
return True

View File

@@ -115,3 +115,43 @@ def f():
else:
return True
return False
def f():
def any(exp):
pass
for x in iterable:
if check(x):
return True
return False
def f():
def all(exp):
pass
for x in iterable:
if check(x):
return False
return True
def f():
x = 1
# SIM110
for x in iterable:
if check(x):
return True
return False
def f():
x = 1
# SIM111
for x in iterable:
if check(x):
return False
return True

View File

@@ -5,3 +5,10 @@ a = True if b != c else False # SIM210
a = True if b + c else False # SIM210
a = False if b else True # OK
def f():
# OK
def bool():
return False
a = True if b else False

View File

@@ -10,6 +10,7 @@ YODA == age # SIM300
YODA > age # SIM300
YODA >= age # SIM300
JediOrder.YODA == age # SIM300
0 < (number - 100) # SIM300
# OK
compare == "yoda"
@@ -24,3 +25,4 @@ age < YODA
age <= YODA
YODA == YODA
age == JediOrder.YODA
(number - 100) > 0

View File

@@ -4,15 +4,22 @@ if TYPE_CHECKING:
pass # TCH005
if False:
pass # TCH005
if 0:
pass # TCH005
def example():
if TYPE_CHECKING:
pass # TYP005
pass # TCH005
return
class Test:
if TYPE_CHECKING:
pass # TYP005
pass # TCH005
x = 2
@@ -23,3 +30,10 @@ if TYPE_CHECKING:
if TYPE_CHECKING:
x: List
if False:
x: List
if 0:
x: List

View File

@@ -0,0 +1,8 @@
# office_helper and tests are both first-party,
# but we want tests and experiments to be separated, in that order
from office_helper.core import CoreState
import tests.common.foo as tcf
from tests.common import async_mock_service
from experiments.starry import *
from experiments.weird import varieties
from office_helper.assistants import entity_registry as er

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
class Thing(object):
name: str
def __init__(self, name: str):
self.name = name

View File

@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object
def main():
my_local_folder_object.get()

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
from typing import Any
from requests import Session
from my_first_party import my_first_party_object
from . import my_local_folder_object

View File

@@ -0,0 +1,13 @@
from numpy import (
cos,
int8,
int16,
int32,
int64,
sin,
tan,
uint8,
uint16,
uint32,
uint64,
)

View File

@@ -1,2 +1,5 @@
[tool.ruff]
line-length = 88
[tool.ruff.isort]
lines-after-imports = 3

View File

@@ -86,3 +86,11 @@ def f():
open("") as ((this, that)),
):
print("hello")
def f():
exponential, base_multiplier = 1, 2
hash_map = {
(exponential := (exponential * base_multiplier) % 3): i + 1 for i in range(2)
}
return hash_map

View File

@@ -0,0 +1,56 @@
def f(): # OK
return
async def f(): # Too many statements (52/50)
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()
print()

View File

@@ -0,0 +1,3 @@
# Too may statements (2/1) for max_statements=1
def f(x):
pass

View File

@@ -0,0 +1,50 @@
# UP035
from collections import Mapping
from collections import Mapping as MAP
from collections import Mapping, Sequence
from collections import Counter, Mapping
from collections import (Counter, Mapping)
from collections import (Counter,
Mapping)
from collections import Counter, \
Mapping
from collections import Counter, Mapping, Sequence
from collections import Mapping as mapping, Counter
if True:
from collections import Mapping, Counter
if True:
if True:
pass
from collections import Mapping, Counter
if True: from collections import Mapping
import os
from collections import Counter, Mapping
import sys
if True:
from collections import (
Mapping,
Callable,
Bad,
Good,
)
from typing import Callable, Match, Pattern, List
if True: from collections import (
Mapping, Counter)
# OK
from a import b

View File

@@ -0,0 +1,180 @@
import sys
if sys.version_info < (3,0):
print("py2")
else:
print("py3")
if sys.version_info < (3,0):
if True:
print("py2!")
else:
print("???")
else:
print("py3")
if sys.version_info < (3,0): print("PY2!")
else: print("PY3!")
if True:
if sys.version_info < (3,0):
print("PY2")
else:
print("PY3")
if sys.version_info < (3,0): print(1 if True else 3)
else:
print("py3")
if sys.version_info < (3,0):
def f():
print("py2")
else:
def f():
print("py3")
print("This the next")
if sys.version_info > (3,0):
print("py3")
else:
print("py2")
x = 1
if sys.version_info > (3,0):
print("py3")
else:
print("py2")
# ohai
x = 1
if sys.version_info > (3,0): print("py3")
else: print("py2")
if sys.version_info > (3,):
print("py3")
else:
print("py2")
if True:
if sys.version_info > (3,):
print("py3")
else:
print("py2")
if sys.version_info < (3,):
print("py2")
else:
print("py3")
def f():
if sys.version_info < (3,0):
try:
yield
finally:
pass
else:
yield
class C:
def g():
pass
if sys.version_info < (3,0):
def f(py2):
pass
else:
def f(py3):
pass
def h():
pass
if True:
if sys.version_info < (3,0):
2
else:
3
# comment
if sys.version_info < (3,0):
def f():
print("py2")
def g():
print("py2")
else:
def f():
print("py3")
def g():
print("py3")
if True:
if sys.version_info > (3,):
print(3)
# comment
print(2+3)
if True:
if sys.version_info > (3,): print(3)
if True:
if sys.version_info > (3,):
print(3)
if True:
if sys.version_info <= (3, 0):
expected_error = []
else:
expected_error = [
"<stdin>:1:5: Generator expression must be parenthesized",
"max(1 for i in range(10), key=lambda x: x+1)",
" ^",
]
if sys.version_info <= (3, 0):
expected_error = []
else:
expected_error = [
"<stdin>:1:5: Generator expression must be parenthesized",
"max(1 for i in range(10), key=lambda x: x+1)",
" ^",
]
if sys.version_info > (3,0):
"""this
is valid"""
"""the indentation on
this line is significant"""
"this is" \
"allowed too"
("so is"
"this for some reason")
if sys.version_info > (3, 0): expected_error = \
[]
if sys.version_info > (3, 0): expected_error = []
if sys.version_info > (3, 0): \
expected_error = []
if True:
if sys.version_info > (3, 0): expected_error = \
[]
if True:
if sys.version_info > (3, 0): expected_error = []
if True:
if sys.version_info > (3, 0): \
expected_error = []

View File

@@ -0,0 +1,76 @@
import sys
if sys.version_info == 2:
2
else:
3
if sys.version_info < (3,):
2
else:
3
if sys.version_info < (3,0):
2
else:
3
if sys.version_info == 3:
3
else:
2
if sys.version_info > (3,):
3
else:
2
if sys.version_info >= (3,):
3
else:
2
from sys import version_info
if version_info > (3,):
3
else:
2
if True:
print(1)
elif sys.version_info < (3,0):
print(2)
else:
print(3)
if True:
print(1)
elif sys.version_info > (3,):
print(3)
else:
print(2)
if True:
print(1)
elif sys.version_info > (3,):
print(3)
def f():
if True:
print(1)
elif sys.version_info > (3,):
print(3)
if True:
print(1)
elif sys.version_info < (3,0):
print(2)
else:
print(3)
def f():
if True:
print(1)
elif sys.version_info > (3,):
print(3)

View File

@@ -0,0 +1,62 @@
import sys
from sys import version_info
if sys.version_info > (3, 5):
3+6
else:
3-5
if version_info > (3, 5):
3+6
else:
3-5
if sys.version_info >= (3,6):
3+6
else:
3-5
if version_info >= (3,6):
3+6
else:
3-5
if sys.version_info < (3,6):
3-5
else:
3+6
if sys.version_info <= (3,5):
3-5
else:
3+6
if sys.version_info <= (3, 5):
3-5
else:
3+6
if sys.version_info >= (3, 5):
pass
if sys.version_info < (3,0):
pass
if True:
if sys.version_info < (3,0):
pass
if sys.version_info < (3,0):
pass
elif False:
pass
if sys.version_info > (3,):
pass
elif False:
pass
if sys.version_info[0] > "2":
3
else:
2

View File

@@ -0,0 +1,24 @@
import sys
if sys.version_info < (3,0):
print("py2")
for item in range(10):
print(f"PY2-{item}")
else :
print("py3")
for item in range(10):
print(f"PY3-{item}")
if False:
if sys.version_info < (3,0):
print("py2")
for item in range(10):
print(f"PY2-{item}")
else :
print("py3")
for item in range(10):
print(f"PY3-{item}")
if sys.version_info < (3,0): print("PY2!")
else : print("PY3!")

View File

@@ -0,0 +1,45 @@
import sys
if True:
if sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
if True:
if foo:
pass
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
if True:
if foo:
pass
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
elif foo:
cmd = [sys.executable, "-m", "test", "-j0"]
if foo:
pass
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
if sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
if foo:
pass
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
else:
cmd = [sys.executable, "-m", "test", "-j0"]
if sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
else:
cmd = [sys.executable, "-m", "test", "-j0"]
if sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
elif foo:
cmd = [sys.executable, "-m", "test", "-j0"]

View File

@@ -86,3 +86,5 @@ import shelve # noqa: RUF100
import sys # noqa: F401, RUF100
print(sys.path)
"shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401

View File

@@ -768,7 +768,7 @@
]
},
"docstring-quotes": {
"description": "Quote style to prefer for docstrings (either \"single\" (`'`) or \"double\" (`\"`)).",
"description": "Quote style to prefer for docstrings (either \"single\" or \"double\").",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -779,7 +779,7 @@
]
},
"inline-quotes": {
"description": "Quote style to prefer for inline strings (either \"single\" (`'`) or \"double\" (`\"`)).",
"description": "Quote style to prefer for inline strings (either \"single\" or \"double\").",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -790,7 +790,7 @@
]
},
"multiline-quotes": {
"description": "Quote style to prefer for multiline strings (either \"single\" (`'`) or \"double\" (`\"`)).",
"description": "Quote style to prefer for multiline strings (either \"single\" or \"double\").",
"anyOf": [
{
"$ref": "#/definitions/Quote"
@@ -937,6 +937,16 @@
"null"
]
},
"forced-separate": {
"description": "A list of modules to separate into auxiliary block(s) of imports, in the order specified.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"known-first-party": {
"description": "A list of modules to consider first-party, regardless of whether they can be identified as such via introspection of the local filesystem.",
"type": [
@@ -957,6 +967,14 @@
"type": "string"
}
},
"lines-after-imports": {
"description": "The number of blank lines to place after imports. -1 for automatic determination.",
"type": [
"integer",
"null"
],
"format": "int"
},
"no-lines-before": {
"description": "A list of sections that should _not_ be delineated from the previous section via empty lines.",
"type": [
@@ -1102,7 +1120,7 @@
"type": "object",
"properties": {
"keep-runtime-typing": {
"description": "Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 (`Optional[str]` -> `str | None`) rewrites even if a file imports `from __future__ import annotations`. Note that this setting is only applicable when the target Python version is below 3.9 and 3.10 respectively.",
"description": "Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 (`Optional[str]` -> `str | None`) rewrites even if a file imports `from __future__ import annotations`. Note that this setting is only applicable when the target Python version is below 3.9 and 3.10 respectively, and enabling it is equivalent to disabling `use-pep585-annotation` (`UP006`) and `use-pep604-annotation` (`UP007`) entirely.",
"type": [
"boolean",
"null"
@@ -1115,7 +1133,7 @@
"type": "object",
"properties": {
"ignore-overlong-task-comments": {
"description": "Whether or not line-length violations (`E501`) should be triggered for comments starting with `task-tags` (by default: [\"TODO\", \"FIXME\", and \"XXX\"]).",
"description": "Whether line-length violations (`E501`) should be triggered for comments starting with `task-tags` (by default: [\"TODO\", \"FIXME\", and \"XXX\"]).",
"type": [
"boolean",
"null"
@@ -1171,6 +1189,15 @@
],
"format": "uint",
"minimum": 0.0
},
"max-statements": {
"description": "Maximum number of statements allowed for a method or a statement (see: `PLR0915`).",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
@@ -1188,14 +1215,14 @@
"Quote": {
"oneOf": [
{
"description": "Use single quotes (`'`).",
"description": "Use single quotes.",
"type": "string",
"enum": [
"single"
]
},
{
"description": "Use double quotes (`\"`).",
"description": "Use double quotes.",
"type": "string",
"enum": [
"double"
@@ -1643,6 +1670,7 @@
"PLR09",
"PLR091",
"PLR0913",
"PLR0915",
"PLR1",
"PLR17",
"PLR170",
@@ -1739,6 +1767,10 @@
"RET506",
"RET507",
"RET508",
"RSE",
"RSE1",
"RSE10",
"RSE102",
"RUF",
"RUF0",
"RUF00",
@@ -1816,6 +1848,10 @@
"SIM4",
"SIM40",
"SIM401",
"SLF",
"SLF0",
"SLF00",
"SLF001",
"T",
"T1",
"T10",
@@ -1892,6 +1928,8 @@
"UP032",
"UP033",
"UP034",
"UP035",
"UP036",
"W",
"W2",
"W29",

View File

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

View File

@@ -58,20 +58,23 @@ fn inner_main() -> Result<ExitCode> {
log_level_args,
} = Args::parse_from(args);
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
eprintln!(
r#"
#[cfg(not(debug_assertions))]
{
let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
eprintln!(
r#"
{}: `ruff` crashed. This indicates a bug in `ruff`. If you could open an issue at:
https://github.com/charliermarsh/ruff/issues/new?title=%5BPanic%5D
quoting the executed command, along with the relevant file contents and `pyproject.toml` settings, we'd be very appreciative!
"#,
"error".red().bold(),
);
default_panic_hook(info);
}));
"error".red().bold(),
);
default_panic_hook(info);
}));
}
let log_level: LogLevel = (&log_level_args).into();
set_up_logging(&log_level)?;

View File

@@ -7,7 +7,7 @@ use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use anyhow::Result;
use colored::Colorize;
use itertools::iterate;
use itertools::{iterate, Itertools};
use ruff::fs::relativize_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
@@ -344,13 +344,16 @@ impl<'a> Printer<'a> {
}
pub fn write_statistics(&self, diagnostics: &Diagnostics) -> Result<()> {
let mut violations = diagnostics
let violations = diagnostics
.messages
.iter()
.map(|message| message.kind.rule())
.sorted()
.dedup()
.collect::<Vec<_>>();
violations.sort();
violations.dedup();
if violations.is_empty() {
return Ok(());
}
let statistics = violations
.iter()

View File

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

View File

@@ -5,6 +5,8 @@ use anyhow::Result;
use ruff::settings::options::Options;
use schemars::schema_for;
use crate::ROOT_DIR;
#[derive(clap::Args)]
pub struct Args {
/// Write the generated table to stdout (rather than to `ruff.schema.json`).
@@ -19,10 +21,7 @@ pub fn main(args: &Args) -> Result<()> {
if args.dry_run {
println!("{schema_string}");
} else {
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("ruff.schema.json");
let file = PathBuf::from(ROOT_DIR).join("ruff.schema.json");
fs::write(file, schema_string.as_bytes())?;
}
Ok(())

View File

@@ -30,6 +30,8 @@ mod utils;
use anyhow::Result;
use clap::{Parser, Subcommand};
const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../");
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]

View File

@@ -5,12 +5,11 @@ use std::path::PathBuf;
use anyhow::Result;
use crate::ROOT_DIR;
pub fn replace_readme_section(content: &str, begin_pragma: &str, end_pragma: &str) -> Result<()> {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let file = PathBuf::from(ROOT_DIR).join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.

View File

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

View File

@@ -52,6 +52,7 @@ pub fn expand<'a>(
::strum_macros::EnumIter,
::strum_macros::EnumString,
::strum_macros::AsRefStr,
::strum_macros::IntoStaticStr,
Debug,
PartialEq,
Eq,

View File

@@ -15,16 +15,31 @@ SECTIONS: list[tuple[str, str]] = [
("FAQ", "faq.md"),
]
DOCUMENTATION_LINK: str = (
"This README is also available as [documentation](https://beta.ruff.rs/docs/)."
)
FATHOM_SCRIPT: str = (
'<script src="https://cdn.usefathom.com/script.js" data-site="DUAEBFLB" defer>'
"</script>"
)
def main() -> None:
"""Generate an MkDocs-compatible `docs` and `mkdocs.yml`."""
with Path("README.md").open(encoding="utf8") as fp:
content = fp.read()
# Remove the documentation link, since we're _in_ the docs.
if DOCUMENTATION_LINK not in content:
msg = "README.md is not in the expected format."
raise ValueError(msg)
content = content.replace(DOCUMENTATION_LINK, "")
Path("docs").mkdir(parents=True, exist_ok=True)
# Split the README.md into sections.
for (title, filename) in 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:
@@ -54,6 +69,11 @@ def main() -> None:
{"FAQ": "faq.md"},
{"Contributing": "contributing.md"},
]
config["extra"] = {"analytics": {"provider": "fathom"}}
Path(".overrides/partials/integrations/analytics").mkdir(parents=True, exist_ok=True)
with Path(".overrides/partials/integrations/analytics/fathom.html").open("w+") as fp:
fp.write(FATHOM_SCRIPT)
with Path("mkdocs.yml").open("w+") as fp:
yaml.safe_dump(config, fp)

View File

@@ -22,3 +22,5 @@ sys.exit(1)
# To be removed once GitHub catches up.
setup(name="ruff", install_requires=[])
if True: a = 1; \
b = 2

View File

@@ -266,8 +266,8 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
}
}
ExprKind::NamedExpr { target, value } => {
visitor.visit_expr(target);
visitor.visit_expr(value);
visitor.visit_expr(target);
}
ExprKind::BinOp { left, op, right } => {
visitor.visit_expr(left);

View File

@@ -36,10 +36,10 @@ use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_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_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -87,6 +87,9 @@ pub struct Checker<'a> {
deferred_functions: Vec<(&'a Stmt, DeferralContext<'a>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext<'a>)>,
deferred_assignments: Vec<DeferralContext<'a>>,
// Body iteration; used to peek at siblings.
body: &'a [Stmt],
body_index: usize,
// Internal, derivative state.
visible_scope: VisibleScope,
in_annotation: bool,
@@ -145,6 +148,9 @@ impl<'a> Checker<'a> {
deferred_functions: vec![],
deferred_lambdas: vec![],
deferred_assignments: vec![],
// Body iteration.
body: &[],
body_index: 0,
// Internal, derivative state.
visible_scope: VisibleScope {
modifier: Modifier::Module,
@@ -580,6 +586,21 @@ where
pylint::rules::property_with_parameters(self, stmt, decorator_list, args);
}
if self.settings.rules.enabled(&Rule::TooManyArgs) {
pylint::rules::too_many_args(self, args, stmt);
}
if self.settings.rules.enabled(&Rule::TooManyStatements) {
if let Some(diagnostic) = pylint::rules::too_many_statements(
stmt,
body,
self.settings.pylint.max_statements,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
}
if self
.settings
.rules
@@ -696,9 +717,6 @@ where
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) {
@@ -1073,6 +1091,15 @@ where
if self.settings.rules.enabled(&Rule::RewriteCElementTree) {
pyupgrade::rules::replace_c_element_tree(self, stmt);
}
if self.settings.rules.enabled(&Rule::ImportReplacements) {
pyupgrade::rules::import_replacements(
self,
stmt,
names,
module.as_ref().map(String::as_str),
level.as_ref(),
);
}
if self.settings.rules.enabled(&Rule::UnnecessaryBuiltinImport) {
if let Some(module) = module.as_deref() {
pyupgrade::rules::unnecessary_builtin_import(self, stmt, module, names);
@@ -1425,6 +1452,15 @@ where
tryceratops::rules::raise_vanilla_args(self, expr);
}
}
if self
.settings
.rules
.enabled(&Rule::UnnecessaryParenOnRaiseException)
{
if let Some(expr) = exc {
flake8_raise::rules::unnecessary_paren_on_raise_exception(self, expr);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
@@ -1476,6 +1512,9 @@ where
self.current_stmt_parent().map(Into::into),
);
}
if self.settings.rules.enabled(&Rule::OutdatedVersionBlock) {
pyupgrade::rules::outdated_version_block(self, stmt, test, body, orelse);
}
}
StmtKind::Assert { test, msg } => {
if self.settings.rules.enabled(&Rule::AssertTuple) {
@@ -1572,7 +1611,11 @@ where
if self.settings.rules.enabled(&Rule::ConvertLoopToAny)
|| self.settings.rules.enabled(&Rule::ConvertLoopToAll)
{
flake8_simplify::rules::convert_for_loop_to_any_all(self, stmt, None);
flake8_simplify::rules::convert_for_loop_to_any_all(
self,
stmt,
self.current_sibling_stmt(),
);
}
if self.settings.rules.enabled(&Rule::KeyInDict) {
flake8_simplify::rules::key_in_dict_for(self, target, iter);
@@ -1901,7 +1944,7 @@ where
if flake8_type_checking::helpers::is_type_checking_block(self, test) {
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
flake8_type_checking::rules::empty_type_checking_block(self, body);
}
let prev_in_type_checking_block = self.in_type_checking_block;
@@ -1990,12 +2033,12 @@ where
ExprKind::Subscript { value, slice, .. } => {
// Ex) Optional[...]
if !self.in_deferred_string_type_definition
&& self.in_annotation
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP604Annotation)
&& (self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.annotations_future_enabled))
&& self.annotations_future_enabled
&& self.in_annotation))
{
pyupgrade::rules::use_pep604_annotation(self, expr, value, slice);
}
@@ -2045,10 +2088,10 @@ where
// Ex) List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.annotations_future_enabled
&& self.in_annotation))
&& typing::is_pep585_builtin(self, expr)
@@ -2090,6 +2133,7 @@ where
ExprKind::Attribute { attr, value, .. } => {
// Ex) typing.List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
@@ -2116,6 +2160,9 @@ where
if self.settings.rules.enabled(&Rule::BannedApi) {
flake8_tidy_imports::banned_api::banned_attribute_access(self, expr);
}
if self.settings.rules.enabled(&Rule::PrivateMemberAccess) {
flake8_self::rules::private_member_access(self, expr);
}
pandas_vet::rules::check_attr(self, attr, value, expr);
}
ExprKind::Call {
@@ -3676,19 +3723,18 @@ where
flake8_pie::rules::no_unnecessary_pass(self, body);
}
if self.settings.rules.enabled(&Rule::ConvertLoopToAny)
|| self.settings.rules.enabled(&Rule::ConvertLoopToAll)
{
for (stmt, sibling) in body.iter().tuple_windows() {
if matches!(stmt.node, StmtKind::For { .. })
&& matches!(sibling.node, StmtKind::Return { .. })
{
flake8_simplify::rules::convert_for_loop_to_any_all(self, stmt, Some(sibling));
}
}
let prev_body = self.body;
let prev_body_index = self.body_index;
self.body = body;
self.body_index = 0;
for stmt in body {
self.visit_stmt(stmt);
self.body_index += 1;
}
visitor::walk_body(self, body);
self.body = prev_body;
self.body_index = prev_body_index;
}
}
@@ -3777,6 +3823,11 @@ impl<'a> Checker<'a> {
self.exprs.iter().rev().nth(2)
}
/// Return the `Stmt` that immediately follows the current `Stmt`, if any.
pub fn current_sibling_stmt(&self) -> Option<&'a Stmt> {
self.body.get(self.body_index + 1)
}
pub fn current_scope(&self) -> &Scope {
&self.scopes[*(self.scope_stack.last().expect("No current scope found"))]
}
@@ -4398,7 +4449,7 @@ impl<'a> Checker<'a> {
fn check_deferred_assignments(&mut self) {
self.deferred_assignments.reverse();
while let Some((scopes, _parents)) = self.deferred_assignments.pop() {
while let Some((scopes, ..)) = self.deferred_assignments.pop() {
let scope_index = scopes[scopes.len() - 1];
let parent_scope_index = scopes[scopes.len() - 2];
if self.settings.rules.enabled(&Rule::UnusedVariable) {
@@ -5201,9 +5252,7 @@ pub fn check_ast(
};
// Iterate over the AST.
for stmt in python_ast {
checker.visit_stmt(stmt);
}
checker.visit_body(python_ast);
// Check any deferred statements.
checker.check_deferred_functions();

View File

@@ -100,8 +100,11 @@ pub fn check_noqa(
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
Directive::All(spaces, start, end) => {
Directive::All(spaces, start_byte, end_byte) => {
if matches.is_empty() {
let start = lines[row][..start_byte].chars().count();
let end = start + lines[row][start_byte..end_byte].chars().count();
let mut diagnostic = Diagnostic::new(
violations::UnusedNOQA { codes: None },
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
@@ -117,7 +120,7 @@ pub fn check_noqa(
diagnostics.push(diagnostic);
}
}
Directive::Codes(spaces, start, end, codes) => {
Directive::Codes(spaces, start_byte, end_byte, codes) => {
let mut disabled_codes = vec![];
let mut unknown_codes = vec![];
let mut unmatched_codes = vec![];
@@ -153,6 +156,9 @@ pub fn check_noqa(
&& unknown_codes.is_empty()
&& unmatched_codes.is_empty())
{
let start = lines[row][..start_byte].chars().count();
let end = start + lines[row][start_byte..end_byte].chars().count();
let mut diagnostic = Diagnostic::new(
violations::UnusedNOQA {
codes: Some(UnusedCodes {

View File

@@ -48,77 +48,73 @@ pub fn check_tokens(
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character || enforce_quotes {
state_machine.consume(tok)
} else {
false
};
if enforce_ambiguous_unicode_character
|| enforce_commented_out_code
|| enforce_invalid_escape_sequence
{
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character {
state_machine.consume(tok)
} else {
false
};
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
} else {
Context::String
}
} else {
Context::String
}
} else {
Context::Comment
},
settings,
autofix,
));
Context::Comment
},
settings,
autofix,
));
}
}
}
// flake8-quotes
if enforce_quotes {
if matches!(tok, Tok::String { .. }) {
if let Some(diagnostic) = flake8_quotes::rules::quotes(
locator,
start,
end,
is_docstring,
settings,
autofix,
) {
if settings.rules.enabled(diagnostic.kind.rule()) {
// eradicate
if enforce_commented_out_code {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
{
diagnostics.push(diagnostic);
}
}
}
}
// eradicate
if enforce_commented_out_code {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
{
diagnostics.push(diagnostic);
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
start,
end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
}
}
}
}
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
start,
end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
}
}
// Q001, Q002, Q003
if enforce_quotes {
diagnostics.extend(
flake8_quotes::rules::from_tokens(tokens, locator, settings, autofix)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}
// ISC001, ISC002

View File

@@ -1,6 +1,6 @@
use anyhow::{bail, Result};
use libcst_native::{
Call, Expr, Expression, Import, ImportFrom, Module, SmallStatement, Statement,
Call, Comparison, Expr, Expression, Import, ImportFrom, Module, SmallStatement, Statement,
};
pub fn match_module(module_text: &str) -> Result<Module> {
@@ -34,7 +34,7 @@ pub fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
bail!("Expected SmallStatement::Import")
}
} else {
bail!("Expected Statement::Simple")
@@ -46,7 +46,7 @@ pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut I
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
bail!("Expected SmallStatement::ImportFrom")
}
} else {
bail!("Expected Statement::Simple")
@@ -57,6 +57,16 @@ pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut
if let Expression::Call(call) = expression {
Ok(call)
} else {
bail!("Expected SmallStatement::Expr")
bail!("Expected Expression::Call")
}
}
pub fn match_comparison<'a, 'b>(
expression: &'a mut Expression<'b>,
) -> Result<&'a mut Comparison<'b>> {
if let Expression::Comparison(comparison) = expression {
Ok(comparison)
} else {
bail!("Expected Expression::Comparison")
}
}

View File

@@ -1,6 +1,7 @@
use std::collections::{BTreeSet, HashMap};
use std::collections::{HashMap, HashSet};
use anyhow::Result;
use itertools::Itertools;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
@@ -38,7 +39,7 @@ pub fn convert(
.expect("Unable to find flake8 section in INI file");
// Extract all referenced rule code prefixes, to power plugin inference.
let mut referenced_codes: BTreeSet<RuleSelector> = BTreeSet::default();
let mut referenced_codes: HashSet<RuleSelector> = HashSet::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
@@ -80,15 +81,15 @@ pub fn convert(
.and_then(|value| {
value
.as_ref()
.map(|value| BTreeSet::from_iter(parser::parse_prefix_codes(value)))
.map(|value| HashSet::from_iter(parser::parse_prefix_codes(value)))
})
.unwrap_or_else(|| resolve_select(&plugins));
let mut ignore = flake8
let mut ignore: HashSet<RuleSelector> = flake8
.get("ignore")
.and_then(|value| {
value
.as_ref()
.map(|value| BTreeSet::from_iter(parser::parse_prefix_codes(value)))
.map(|value| HashSet::from_iter(parser::parse_prefix_codes(value)))
})
.unwrap_or_default();
@@ -349,8 +350,18 @@ pub fn convert(
}
// Deduplicate and sort.
options.select = Some(Vec::from_iter(select));
options.ignore = Some(Vec::from_iter(ignore));
options.select = Some(
select
.into_iter()
.sorted_by_key(RuleSelector::short_code)
.collect(),
);
options.ignore = Some(
ignore
.into_iter()
.sorted_by_key(RuleSelector::short_code)
.collect(),
);
if flake8_annotations != flake8_annotations::settings::Options::default() {
options.flake8_annotations = Some(flake8_annotations);
}
@@ -414,8 +425,8 @@ pub fn convert(
/// Resolve the set of enabled `RuleSelector` values for the given
/// plugins.
fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleSelector> {
let mut select: BTreeSet<_> = DEFAULT_SELECTORS.iter().cloned().collect();
fn resolve_select(plugins: &[Plugin]) -> HashSet<RuleSelector> {
let mut select: HashSet<_> = DEFAULT_SELECTORS.iter().cloned().collect();
select.extend(plugins.iter().map(Plugin::selector));
select
}
@@ -446,7 +457,7 @@ mod tests {
.iter()
.cloned()
.chain(plugins)
.sorted()
.sorted_by_key(RuleSelector::short_code)
.collect(),
),
..Options::default()

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeSet, HashMap};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
@@ -293,7 +293,7 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
///
/// For example, if the user ignores `ANN101`, we should infer that
/// `flake8-annotations` is active.
pub fn infer_plugins_from_codes(selectors: &BTreeSet<RuleSelector>) -> Vec<Plugin> {
pub fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec<Plugin> {
// Ignore cases in which we've knowingly changed rule prefixes.
[
Plugin::Flake82020,

View File

@@ -1,8 +1,10 @@
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
@@ -34,10 +36,28 @@ pub(crate) fn ignores_from_path<'a>(
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
.filter(|(absolute, basename, _)| {
basename.is_match(file_basename) || absolute.is_match(file_path)
.filter_map(|(absolute, basename, codes)| {
if basename.is_match(file_basename) {
debug!(
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
path,
basename.deref().glob().regex(),
&**codes
);
return Some(codes.iter());
}
if absolute.is_match(file_path) {
debug!(
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
path,
absolute.deref().glob().regex(),
&**codes
);
return Some(codes.iter());
}
None
})
.flat_map(|(_, _, codes)| codes.iter())
.flatten()
.collect())
}
@@ -61,7 +81,8 @@ pub fn normalize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root:
}
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path(path: &Path) -> String {
pub fn relativize_path(path: impl AsRef<Path>) -> String {
let path = path.as_ref();
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
return format!("{}", path.display());
}

View File

@@ -67,3 +67,6 @@ cfg_if! {
pub use lib_wasm::check;
}
}
#[cfg(test)]
mod test;

View File

@@ -16,8 +16,6 @@ use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::message::{Message, Source};
use crate::noqa::add_noqa;
#[cfg(test)]
use crate::packaging::detect_package_root;
use crate::registry::{Diagnostic, LintSource, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::{Indexer, Locator, Stylist};
@@ -222,8 +220,8 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
path,
&diagnostics,
&contents,
indexer.commented_lines(),
&directives.noqa_line_for,
&settings.external,
stylist.line_ending(),
)
}
@@ -382,77 +380,3 @@ quoting the contents of `{}`, along with the `pyproject.toml` settings and execu
return Ok((contents, fixed, messages));
}
}
#[cfg(test)]
pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = Locator::new(&contents);
let stylist = Stylist::from_contents(&contents, &locator);
let indexer: Indexer = tokens.as_slice().into();
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let mut diagnostics = check_path(
path,
path.parent()
.and_then(|parent| detect_package_root(parent, &settings.namespace_packages)),
&contents,
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Enabled,
flags::Noqa::Enabled,
)?;
// Detect autofixes that don't converge after multiple iterations.
if diagnostics
.iter()
.any(|diagnostic| diagnostic.fix.is_some())
{
let max_iterations = 10;
let mut contents = contents.clone();
let mut iterations = 0;
loop {
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = Locator::new(&contents);
let stylist = Stylist::from_contents(&contents, &locator);
let indexer: Indexer = tokens.as_slice().into();
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let diagnostics = check_path(
path,
None,
&contents,
tokens,
&locator,
&stylist,
&indexer,
&directives,
settings,
flags::Autofix::Enabled,
flags::Noqa::Enabled,
)?;
if let Some((fixed_contents, _)) = fix_file(&diagnostics, &locator) {
if iterations < max_iterations {
iterations += 1;
contents = fixed_contents.to_string();
} else {
panic!(
"Failed to converge after {max_iterations} iterations. This likely \
indicates a bug in the implementation of the fix."
);
}
} else {
break;
}
}
}
diagnostics.sort_by_key(|diagnostic| diagnostic.location);
Ok(diagnostics)
}

View File

@@ -10,7 +10,6 @@ use rustc_hash::{FxHashMap, FxHashSet};
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
@@ -81,12 +80,17 @@ pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
external: &HashableHashSet<String>,
line_ending: &LineEnding,
) -> Result<usize> {
let (count, output) =
add_noqa_inner(diagnostics, contents, noqa_line_for, external, line_ending);
let (count, output) = add_noqa_inner(
diagnostics,
contents,
commented_lines,
noqa_line_for,
line_ending,
);
fs::write(path, output)?;
Ok(count)
}
@@ -94,31 +98,66 @@ pub fn add_noqa(
fn add_noqa_inner(
diagnostics: &[Diagnostic],
contents: &str,
commented_lines: &[usize],
noqa_line_for: &IntMap<usize, usize>,
external: &HashableHashSet<String>,
line_ending: &LineEnding,
) -> (usize, String) {
let mut matches_by_line: FxHashMap<usize, FxHashSet<&Rule>> = FxHashMap::default();
for (lineno, line) in contents.lines().enumerate() {
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(line) {
return (0, contents.to_string());
}
// Grab the noqa (logical) line number for the current (physical) line.
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let mut codes: FxHashSet<&Rule> = FxHashSet::default();
for diagnostic in diagnostics {
// TODO(charlie): Consider respecting parent `noqa` directives. For now, we'll
// add a `noqa` for every diagnostic, on its own line. This could lead to
// duplication, whereby some parent `noqa` directives become
// redundant.
if diagnostic.location.row() == lineno + 1 {
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
}
// Is the diagnostic ignored by a `noqa` directive on the same line?
let diagnostic_lineno = diagnostic.location.row();
let noqa_lineno = noqa_line_for
.get(&diagnostic_lineno)
.unwrap_or(&diagnostic_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
codes.insert(diagnostic.kind.rule());
}
}
// Grab the noqa (logical) line number for the current (physical) line.
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
if !codes.is_empty() {
matches_by_line
.entry(noqa_lineno)
@@ -151,28 +190,19 @@ fn add_noqa_inner(
output.push_str(line_ending);
count += 1;
}
Directive::All(_, start, _) => {
// Add existing content.
output.push_str(line[..start].trim_end());
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> =
rules.iter().map(|r| r.code()).sorted_unstable().collect();
let suffix = codes.join(", ");
output.push_str(&suffix);
Directive::All(..) => {
// Leave the line as-is.
output.push_str(line);
output.push_str(line_ending);
count += 1;
}
Directive::Codes(_, start, _, existing) => {
Directive::Codes(_, start_byte, _, existing) => {
println!("existing: {:?}", existing);
// Reconstruct the line based on the preserved rule codes.
// This enables us to tally the number of edits.
let mut formatted = String::new();
let mut formatted = String::with_capacity(line.len());
// Add existing content.
formatted.push_str(line[..start].trim_end());
formatted.push_str(line[..start_byte].trim_end());
// Add `noqa` directive.
formatted.push_str(" # noqa: ");
@@ -181,7 +211,7 @@ fn add_noqa_inner(
let codes: Vec<&str> = rules
.iter()
.map(|r| r.code())
.chain(existing.into_iter().filter(|code| external.contains(*code)))
.chain(existing.into_iter())
.sorted_unstable()
.collect();
let suffix = codes.join(", ");
@@ -212,7 +242,6 @@ mod tests {
use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX};
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
use crate::violations;
@@ -234,13 +263,13 @@ mod tests {
fn modification() {
let diagnostics = vec![];
let contents = "x = 1";
let commented_lines = vec![];
let noqa_line_for = IntMap::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&noqa_line_for,
&external,
&LineEnding::Lf,
);
assert_eq!(count, 0);
@@ -253,13 +282,13 @@ mod tests {
Range::new(Location::new(1, 0), Location::new(1, 0)),
)];
let contents = "x = 1";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&noqa_line_for,
&external,
&LineEnding::Lf,
);
assert_eq!(count, 1);
@@ -278,13 +307,13 @@ mod tests {
),
];
let contents = "x = 1 # noqa: E741\n";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&noqa_line_for,
&external,
&LineEnding::Lf,
);
assert_eq!(count, 1);
@@ -303,16 +332,16 @@ mod tests {
),
];
let contents = "x = 1 # noqa";
let commented_lines = vec![1];
let noqa_line_for = IntMap::default();
let external = HashableHashSet::default();
let (count, output) = add_noqa_inner(
&diagnostics,
contents,
&commented_lines,
&noqa_line_for,
&external,
&LineEnding::Lf,
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa\n");
}
}

View File

@@ -119,50 +119,26 @@ pub fn detect_package_roots<'a>(
mod tests {
use std::path::PathBuf;
use crate::packaging::detect_package_root;
use crate::{packaging::detect_package_root, test::test_resource_path};
#[test]
fn package_detection() {
assert_eq!(
detect_package_root(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/package/src/package")
.as_path(),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/package/src/package")
.as_path()
)
detect_package_root(&test_resource_path("package/src/package"), &[],),
Some(test_resource_path("package/src/package").as_path())
);
assert_eq!(
detect_package_root(&test_resource_path("project/python_modules/core/core"), &[],),
Some(test_resource_path("project/python_modules/core/core").as_path())
);
assert_eq!(
detect_package_root(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/python_modules/core/core")
.as_path(),
&test_resource_path("project/examples/docs/docs/concepts"),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/python_modules/core/core")
.as_path()
)
);
assert_eq!(
detect_package_root(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/examples/docs/docs/concepts")
.as_path(),
&[],
),
Some(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/project/examples/docs/docs")
.as_path()
)
Some(test_resource_path("project/examples/docs/docs").as_path())
);
assert_eq!(

View File

@@ -7,7 +7,6 @@ use strum_macros::{AsRefStr, EnumIter};
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::rule_selector::{prefix_to_selector, RuleSelector};
use crate::violation::Violation;
use crate::{rules, violations};
@@ -94,58 +93,59 @@ ruff_macros::define_rule_mapping!(
PLW0120 => violations::UselessElseOnLoop,
PLW0602 => violations::GlobalVariableNotAssigned,
PLR0913 => rules::pylint::rules::TooManyArgs,
PLR0915 => rules::pylint::rules::TooManyStatements,
// flake8-builtins
A001 => violations::BuiltinVariableShadowing,
A002 => violations::BuiltinArgumentShadowing,
A003 => violations::BuiltinAttributeShadowing,
A001 => rules::flake8_builtins::rules::BuiltinVariableShadowing,
A002 => rules::flake8_builtins::rules::BuiltinArgumentShadowing,
A003 => rules::flake8_builtins::rules::BuiltinAttributeShadowing,
// flake8-bugbear
B002 => violations::UnaryPrefixIncrement,
B003 => violations::AssignmentToOsEnviron,
B004 => violations::UnreliableCallableCheck,
B005 => violations::StripWithMultiCharacters,
B006 => violations::MutableArgumentDefault,
B007 => violations::UnusedLoopControlVariable,
B008 => violations::FunctionCallArgumentDefault,
B009 => violations::GetAttrWithConstant,
B010 => violations::SetAttrWithConstant,
B011 => violations::DoNotAssertFalse,
B012 => violations::JumpStatementInFinally,
B013 => violations::RedundantTupleInExceptionHandler,
B014 => violations::DuplicateHandlerException,
B015 => violations::UselessComparison,
B016 => violations::CannotRaiseLiteral,
B017 => violations::NoAssertRaisesException,
B018 => violations::UselessExpression,
B019 => violations::CachedInstanceMethod,
B020 => violations::LoopVariableOverridesIterator,
B021 => violations::FStringDocstring,
B022 => violations::UselessContextlibSuppress,
B023 => violations::FunctionUsesLoopVariable,
B024 => violations::AbstractBaseClassWithoutAbstractMethod,
B025 => violations::DuplicateTryBlockException,
B026 => violations::StarArgUnpackingAfterKeywordArg,
B027 => violations::EmptyMethodWithoutAbstractDecorator,
B904 => violations::RaiseWithoutFromInsideExcept,
B905 => violations::ZipWithoutExplicitStrict,
B002 => rules::flake8_bugbear::rules::UnaryPrefixIncrement,
B003 => rules::flake8_bugbear::rules::AssignmentToOsEnviron,
B004 => rules::flake8_bugbear::rules::UnreliableCallableCheck,
B005 => rules::flake8_bugbear::rules::StripWithMultiCharacters,
B006 => rules::flake8_bugbear::rules::MutableArgumentDefault,
B007 => rules::flake8_bugbear::rules::UnusedLoopControlVariable,
B008 => rules::flake8_bugbear::rules::FunctionCallArgumentDefault,
B009 => rules::flake8_bugbear::rules::GetAttrWithConstant,
B010 => rules::flake8_bugbear::rules::SetAttrWithConstant,
B011 => rules::flake8_bugbear::rules::DoNotAssertFalse,
B012 => rules::flake8_bugbear::rules::JumpStatementInFinally,
B013 => rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler,
B014 => rules::flake8_bugbear::rules::DuplicateHandlerException,
B015 => rules::flake8_bugbear::rules::UselessComparison,
B016 => rules::flake8_bugbear::rules::CannotRaiseLiteral,
B017 => rules::flake8_bugbear::rules::NoAssertRaisesException,
B018 => rules::flake8_bugbear::rules::UselessExpression,
B019 => rules::flake8_bugbear::rules::CachedInstanceMethod,
B020 => rules::flake8_bugbear::rules::LoopVariableOverridesIterator,
B021 => rules::flake8_bugbear::rules::FStringDocstring,
B022 => rules::flake8_bugbear::rules::UselessContextlibSuppress,
B023 => rules::flake8_bugbear::rules::FunctionUsesLoopVariable,
B024 => rules::flake8_bugbear::rules::AbstractBaseClassWithoutAbstractMethod,
B025 => rules::flake8_bugbear::rules::DuplicateTryBlockException,
B026 => rules::flake8_bugbear::rules::StarArgUnpackingAfterKeywordArg,
B027 => rules::flake8_bugbear::rules::EmptyMethodWithoutAbstractDecorator,
B904 => rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept,
B905 => rules::flake8_bugbear::rules::ZipWithoutExplicitStrict,
// flake8-blind-except
BLE001 => violations::BlindExcept,
BLE001 => rules::flake8_blind_except::rules::BlindExcept,
// flake8-comprehensions
C400 => violations::UnnecessaryGeneratorList,
C401 => violations::UnnecessaryGeneratorSet,
C402 => violations::UnnecessaryGeneratorDict,
C403 => violations::UnnecessaryListComprehensionSet,
C404 => violations::UnnecessaryListComprehensionDict,
C405 => violations::UnnecessaryLiteralSet,
C406 => violations::UnnecessaryLiteralDict,
C408 => violations::UnnecessaryCollectionCall,
C409 => violations::UnnecessaryLiteralWithinTupleCall,
C410 => violations::UnnecessaryLiteralWithinListCall,
C411 => violations::UnnecessaryListCall,
C413 => violations::UnnecessaryCallAroundSorted,
C414 => violations::UnnecessaryDoubleCastOrProcess,
C415 => violations::UnnecessarySubscriptReversal,
C416 => violations::UnnecessaryComprehension,
C417 => violations::UnnecessaryMap,
C400 => rules::flake8_comprehensions::rules::UnnecessaryGeneratorList,
C401 => rules::flake8_comprehensions::rules::UnnecessaryGeneratorSet,
C402 => rules::flake8_comprehensions::rules::UnnecessaryGeneratorDict,
C403 => rules::flake8_comprehensions::rules::UnnecessaryListComprehensionSet,
C404 => rules::flake8_comprehensions::rules::UnnecessaryListComprehensionDict,
C405 => rules::flake8_comprehensions::rules::UnnecessaryLiteralSet,
C406 => rules::flake8_comprehensions::rules::UnnecessaryLiteralDict,
C408 => rules::flake8_comprehensions::rules::UnnecessaryCollectionCall,
C409 => rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinTupleCall,
C410 => rules::flake8_comprehensions::rules::UnnecessaryLiteralWithinListCall,
C411 => rules::flake8_comprehensions::rules::UnnecessaryListCall,
C413 => rules::flake8_comprehensions::rules::UnnecessaryCallAroundSorted,
C414 => rules::flake8_comprehensions::rules::UnnecessaryDoubleCastOrProcess,
C415 => rules::flake8_comprehensions::rules::UnnecessarySubscriptReversal,
C416 => rules::flake8_comprehensions::rules::UnnecessaryComprehension,
C417 => rules::flake8_comprehensions::rules::UnnecessaryMap,
// flake8-debugger
T100 => violations::Debugger,
// mccabe
@@ -170,10 +170,10 @@ ruff_macros::define_rule_mapping!(
T201 => violations::PrintFound,
T203 => violations::PPrintFound,
// flake8-quotes
Q000 => violations::BadQuotesInlineString,
Q001 => violations::BadQuotesMultilineString,
Q002 => violations::BadQuotesDocstring,
Q003 => violations::AvoidQuoteEscape,
Q000 => rules::flake8_quotes::rules::BadQuotesInlineString,
Q001 => rules::flake8_quotes::rules::BadQuotesMultilineString,
Q002 => rules::flake8_quotes::rules::BadQuotesDocstring,
Q003 => rules::flake8_quotes::rules::AvoidQuoteEscape,
// flake8-annotations
ANN001 => violations::MissingTypeFunctionArgument,
ANN002 => violations::MissingTypeArgs,
@@ -256,6 +256,8 @@ ruff_macros::define_rule_mapping!(
UP032 => violations::FString,
UP033 => violations::FunctoolsCache,
UP034 => violations::ExtraneousParentheses,
UP035 => rules::pyupgrade::rules::ImportReplacements,
UP036 => rules::pyupgrade::rules::OutdatedVersionBlock,
// pydocstyle
D100 => violations::PublicModule,
D101 => violations::PublicClass,
@@ -392,26 +394,26 @@ ruff_macros::define_rule_mapping!(
PT003 => rules::flake8_pytest_style::rules::ExtraneousScopeFunction,
PT004 => rules::flake8_pytest_style::rules::MissingFixtureNameUnderscore,
PT005 => rules::flake8_pytest_style::rules::IncorrectFixtureNameUnderscore,
PT006 => violations::ParametrizeNamesWrongType,
PT007 => violations::ParametrizeValuesWrongType,
PT008 => violations::PatchWithLambda,
PT009 => violations::UnittestAssertion,
PT010 => violations::RaisesWithoutException,
PT011 => violations::RaisesTooBroad,
PT012 => violations::RaisesWithMultipleStatements,
PT013 => violations::IncorrectPytestImport,
PT015 => violations::AssertAlwaysFalse,
PT016 => violations::FailWithoutMessage,
PT017 => violations::AssertInExcept,
PT018 => violations::CompositeAssertion,
PT006 => rules::flake8_pytest_style::rules::ParametrizeNamesWrongType,
PT007 => rules::flake8_pytest_style::rules::ParametrizeValuesWrongType,
PT008 => rules::flake8_pytest_style::rules::PatchWithLambda,
PT009 => rules::flake8_pytest_style::rules::UnittestAssertion,
PT010 => rules::flake8_pytest_style::rules::RaisesWithoutException,
PT011 => rules::flake8_pytest_style::rules::RaisesTooBroad,
PT012 => rules::flake8_pytest_style::rules::RaisesWithMultipleStatements,
PT013 => rules::flake8_pytest_style::rules::IncorrectPytestImport,
PT015 => rules::flake8_pytest_style::rules::AssertAlwaysFalse,
PT016 => rules::flake8_pytest_style::rules::FailWithoutMessage,
PT017 => rules::flake8_pytest_style::rules::AssertInExcept,
PT018 => rules::flake8_pytest_style::rules::CompositeAssertion,
PT019 => rules::flake8_pytest_style::rules::FixtureParamWithoutValue,
PT020 => rules::flake8_pytest_style::rules::DeprecatedYieldFixture,
PT021 => rules::flake8_pytest_style::rules::FixtureFinalizerCallback,
PT022 => rules::flake8_pytest_style::rules::UselessYieldFixture,
PT023 => violations::IncorrectMarkParenthesesStyle,
PT023 => rules::flake8_pytest_style::rules::IncorrectMarkParenthesesStyle,
PT024 => rules::flake8_pytest_style::rules::UnnecessaryAsyncioMarkOnFixture,
PT025 => rules::flake8_pytest_style::rules::ErroneousUseFixturesOnFixture,
PT026 => violations::UseFixturesWithoutParameters,
PT026 => rules::flake8_pytest_style::rules::UseFixturesWithoutParameters,
// flake8-pie
PIE790 => rules::flake8_pie::rules::NoUnnecessaryPass,
PIE794 => rules::flake8_pie::rules::DupeClassFieldDefinitions,
@@ -481,6 +483,10 @@ ruff_macros::define_rule_mapping!(
G101 => rules::flake8_logging_format::violations::LoggingExtraAttrClash,
G201 => rules::flake8_logging_format::violations::LoggingExcInfo,
G202 => rules::flake8_logging_format::violations::LoggingRedundantExcInfo,
// flake8-raise
RSE102 => rules::flake8_raise::rules::UnnecessaryParenOnRaiseException,
// flake8-self
SLF001 => rules::flake8_self::rules::PrivateMemberAccess,
// ruff
RUF001 => violations::AmbiguousUnicodeCharacterString,
RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
@@ -610,6 +616,12 @@ pub enum Linter {
/// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
#[prefix = "TRY"]
Tryceratops,
/// [flake8-raise](https://pypi.org/project/flake8-raise/)
#[prefix = "RSE"]
Flake8Raise,
/// [flake8-self](https://pypi.org/project/flake8-self/)
#[prefix = "SLF"]
Flake8Self,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
@@ -633,27 +645,21 @@ pub trait RuleNamespace: Sized {
}
/// The prefix, name and selector for an upstream linter category.
pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleSelector);
// TODO(martin): Move these constant definitions back to Linter::categories impl
// once RuleSelector is an enum with a Linter variant
const PYCODESTYLE_CATEGORIES: &[LinterCategory] = &[
LinterCategory("E", "Error", prefix_to_selector(RuleCodePrefix::E)),
LinterCategory("W", "Warning", prefix_to_selector(RuleCodePrefix::W)),
];
const PYLINT_CATEGORIES: &[LinterCategory] = &[
LinterCategory("PLC", "Convention", prefix_to_selector(RuleCodePrefix::PLC)),
LinterCategory("PLE", "Error", prefix_to_selector(RuleCodePrefix::PLE)),
LinterCategory("PLR", "Refactor", prefix_to_selector(RuleCodePrefix::PLR)),
LinterCategory("PLW", "Warning", prefix_to_selector(RuleCodePrefix::PLW)),
];
pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleCodePrefix);
impl Linter {
pub fn categories(&self) -> Option<&'static [LinterCategory]> {
match self {
Linter::Pycodestyle => Some(PYCODESTYLE_CATEGORIES),
Linter::Pylint => Some(PYLINT_CATEGORIES),
Linter::Pycodestyle => Some(&[
LinterCategory("E", "Error", RuleCodePrefix::E),
LinterCategory("W", "Warning", RuleCodePrefix::W),
]),
Linter::Pylint => Some(&[
LinterCategory("PLC", "Convention", RuleCodePrefix::PLC),
LinterCategory("PLE", "Error", RuleCodePrefix::PLE),
LinterCategory("PLR", "Refactor", RuleCodePrefix::PLR),
LinterCategory("PLW", "Warning", RuleCodePrefix::PLW),
]),
_ => None,
}
}

View File

@@ -11,7 +11,7 @@ use strum_macros::EnumIter;
use crate::registry::{Rule, RuleCodePrefix, RuleIter};
use crate::rule_redirects::get_redirect;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RuleSelector {
/// All rules
All,
@@ -49,15 +49,21 @@ pub enum ParseError {
Unknown(String),
}
impl RuleSelector {
pub fn short_code(&self) -> &'static str {
match self {
RuleSelector::All => "ALL",
RuleSelector::Prefix { prefix, .. } => prefix.into(),
}
}
}
impl Serialize for RuleSelector {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
RuleSelector::All => serializer.serialize_str("ALL"),
RuleSelector::Prefix { prefix, .. } => prefix.serialize(serializer),
}
serializer.serialize_str(self.short_code())
}
}

View File

@@ -9,17 +9,15 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::CommentedOutCode, Path::new("ERA001.py"); "ERA001")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/eradicate")
.join(path)
.as_path(),
Path::new("eradicate").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);

View File

@@ -8,8 +8,8 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::SysVersionSlice3Referenced, Path::new("YTT101.py"); "YTT101")]
@@ -25,9 +25,7 @@ mod tests {
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_2020")
.join(path)
.as_path(),
Path::new("flake8_2020").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);

View File

@@ -11,14 +11,14 @@ mod tests {
use anyhow::Result;
use crate::assert_yaml_snapshot;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
#[test]
fn defaults() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
Path::new("flake8_annotations/annotation_presence.py"),
&Settings {
..Settings::for_rules(vec![
Rule::MissingTypeFunctionArgument,
@@ -42,7 +42,7 @@ mod tests {
#[test]
fn suppress_dummy_args() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"),
Path::new("flake8_annotations/suppress_dummy_args.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
@@ -66,7 +66,7 @@ mod tests {
#[test]
fn mypy_init_return() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"),
Path::new("flake8_annotations/mypy_init_return.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: true,
@@ -90,7 +90,7 @@ mod tests {
#[test]
fn suppress_none_returning() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"),
Path::new("flake8_annotations/suppress_none_returning.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
@@ -114,7 +114,7 @@ mod tests {
#[test]
fn allow_star_arg_any() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_star_arg_any.py"),
Path::new("flake8_annotations/allow_star_arg_any.py"),
&Settings {
flake8_annotations: super::settings::Settings {
mypy_init_return: false,
@@ -132,7 +132,7 @@ mod tests {
#[test]
fn allow_overload() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_overload.py"),
Path::new("flake8_annotations/allow_overload.py"),
&Settings {
..Settings::for_rules(vec![
Rule::MissingReturnTypePublicFunction,
@@ -150,7 +150,7 @@ mod tests {
#[test]
fn allow_nested_overload() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_nested_overload.py"),
Path::new("flake8_annotations/allow_nested_overload.py"),
&Settings {
..Settings::for_rules(vec![
Rule::MissingReturnTypePublicFunction,

View File

@@ -11,9 +11,9 @@ mod tests {
use test_case::test_case;
use crate::assert_yaml_snapshot;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::AssertUsed, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecUsed, Path::new("S102.py"); "S102")]
@@ -35,9 +35,7 @@ mod tests {
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bandit")
.join(path)
.as_path(),
Path::new("flake8_bandit").join(path).as_path(),
&Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
@@ -47,7 +45,7 @@ mod tests {
#[test]
fn check_hardcoded_tmp_additional_dirs() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bandit/S108.py"),
Path::new("flake8_bandit/S108.py"),
&Settings {
flake8_bandit: super::settings::Settings {
hardcoded_tmp_directory: vec![
@@ -68,7 +66,7 @@ mod tests {
#[test]
fn check_typed_exception() -> Result<()> {
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bandit/S110.py"),
Path::new("flake8_bandit/S110.py"),
&Settings {
flake8_bandit: super::settings::Settings {
check_typed_exception: true,

View File

@@ -6,7 +6,7 @@ use crate::registry::Diagnostic;
use crate::violations;
fn check_password_kwarg(arg: &Located<ArgData>, default: &Expr) -> Option<Diagnostic> {
let string = string_literal(default)?;
let string = string_literal(default).filter(|string| !string.is_empty())?;
let kwarg_name = &arg.node.arg;
if !matches_password_name(kwarg_name) {
return None;

View File

@@ -10,7 +10,7 @@ pub fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> {
keywords
.iter()
.filter_map(|keyword| {
let string = string_literal(&keyword.node.value)?;
let string = string_literal(&keyword.node.value).filter(|string| !string.is_empty())?;
let arg = keyword.node.arg.as_ref()?;
if !matches_password_name(arg) {
return None;

View File

@@ -30,7 +30,7 @@ pub fn compare_to_hardcoded_password_string(left: &Expr, comparators: &[Expr]) -
comparators
.iter()
.filter_map(|comp| {
let string = string_literal(comp)?;
let string = string_literal(comp).filter(|string| !string.is_empty())?;
if !is_password_target(left) {
return None;
}
@@ -46,7 +46,7 @@ pub fn compare_to_hardcoded_password_string(left: &Expr, comparators: &[Expr]) -
/// S105
pub fn assign_hardcoded_password_string(value: &Expr, targets: &[Expr]) -> Option<Diagnostic> {
if let Some(string) = string_literal(value) {
if let Some(string) = string_literal(value).filter(|string| !string.is_empty()) {
for target in targets {
if is_password_target(target) {
return Some(Diagnostic::new(

View File

@@ -21,14 +21,14 @@ fn default_tmp_dirs() -> Vec<String> {
pub struct Options {
#[option(
default = "[\"/tmp\", \"/var/tmp\", \"/dev/shm\"]",
value_type = "Vec<String>",
value_type = "list[str]",
example = "hardcoded-tmp-directory = [\"/foo/bar\"]"
)]
/// A list of directories to consider temporary.
pub hardcoded_tmp_directory: Option<Vec<String>>,
#[option(
default = "[]",
value_type = "Vec<String>",
value_type = "list[str]",
example = "extend-hardcoded-tmp-directory = [\"/foo/bar\"]"
)]
/// A list of directories to consider temporary, in addition to those

View File

@@ -6,10 +6,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 12
row: 13
column: 11
end_location:
row: 12
row: 13
column: 19
fix: ~
parent: ~
@@ -17,10 +17,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 13
row: 14
column: 8
end_location:
row: 13
row: 14
column: 16
fix: ~
parent: ~
@@ -28,10 +28,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 14
row: 15
column: 9
end_location:
row: 14
row: 15
column: 17
fix: ~
parent: ~
@@ -39,10 +39,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 15
row: 16
column: 6
end_location:
row: 15
row: 16
column: 14
fix: ~
parent: ~
@@ -50,10 +50,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 16
row: 17
column: 9
end_location:
row: 16
row: 17
column: 17
fix: ~
parent: ~
@@ -61,10 +61,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 17
row: 18
column: 8
end_location:
row: 17
row: 18
column: 16
fix: ~
parent: ~
@@ -72,22 +72,11 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 18
row: 19
column: 10
end_location:
row: 18
column: 18
fix: ~
parent: ~
- kind:
HardcodedPasswordString:
string: s3cr3t
location:
row: 19
column: 18
end_location:
row: 19
column: 26
fix: ~
parent: ~
- kind:
@@ -105,10 +94,21 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 22
row: 21
column: 18
end_location:
row: 21
column: 26
fix: ~
parent: ~
- kind:
HardcodedPasswordString:
string: s3cr3t
location:
row: 23
column: 16
end_location:
row: 22
row: 23
column: 24
fix: ~
parent: ~
@@ -116,10 +116,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 23
row: 24
column: 12
end_location:
row: 23
row: 24
column: 20
fix: ~
parent: ~
@@ -127,10 +127,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 24
row: 25
column: 14
end_location:
row: 24
row: 25
column: 22
fix: ~
parent: ~
@@ -138,10 +138,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 25
row: 26
column: 11
end_location:
row: 25
row: 26
column: 19
fix: ~
parent: ~
@@ -149,10 +149,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 26
row: 27
column: 14
end_location:
row: 26
row: 27
column: 22
fix: ~
parent: ~
@@ -160,10 +160,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 27
row: 28
column: 13
end_location:
row: 27
row: 28
column: 21
fix: ~
parent: ~
@@ -171,22 +171,11 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 28
row: 29
column: 15
end_location:
row: 28
column: 23
fix: ~
parent: ~
- kind:
HardcodedPasswordString:
string: s3cr3t
location:
row: 29
column: 23
end_location:
row: 29
column: 31
fix: ~
parent: ~
- kind:
@@ -204,10 +193,21 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 34
row: 31
column: 23
end_location:
row: 31
column: 31
fix: ~
parent: ~
- kind:
HardcodedPasswordString:
string: s3cr3t
location:
row: 35
column: 15
end_location:
row: 34
row: 35
column: 23
fix: ~
parent: ~
@@ -215,10 +215,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 38
row: 39
column: 19
end_location:
row: 38
row: 39
column: 27
fix: ~
parent: ~
@@ -226,10 +226,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 39
row: 40
column: 16
end_location:
row: 39
row: 40
column: 24
fix: ~
parent: ~
@@ -237,10 +237,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 40
row: 41
column: 17
end_location:
row: 40
row: 41
column: 25
fix: ~
parent: ~
@@ -248,10 +248,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 41
row: 42
column: 14
end_location:
row: 41
row: 42
column: 22
fix: ~
parent: ~
@@ -259,10 +259,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 42
row: 43
column: 17
end_location:
row: 42
row: 43
column: 25
fix: ~
parent: ~
@@ -270,10 +270,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 43
row: 44
column: 16
end_location:
row: 43
row: 44
column: 24
fix: ~
parent: ~
@@ -281,10 +281,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 44
row: 45
column: 18
end_location:
row: 44
row: 45
column: 26
fix: ~
parent: ~
@@ -292,10 +292,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 46
row: 47
column: 12
end_location:
row: 46
row: 47
column: 20
fix: ~
parent: ~
@@ -303,10 +303,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 47
row: 48
column: 9
end_location:
row: 47
row: 48
column: 17
fix: ~
parent: ~
@@ -314,10 +314,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 48
row: 49
column: 10
end_location:
row: 48
row: 49
column: 18
fix: ~
parent: ~
@@ -325,10 +325,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 49
row: 50
column: 7
end_location:
row: 49
row: 50
column: 15
fix: ~
parent: ~
@@ -336,10 +336,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 50
row: 51
column: 10
end_location:
row: 50
row: 51
column: 18
fix: ~
parent: ~
@@ -347,10 +347,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 51
row: 52
column: 9
end_location:
row: 51
row: 52
column: 17
fix: ~
parent: ~
@@ -358,10 +358,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 52
row: 53
column: 11
end_location:
row: 52
row: 53
column: 19
fix: ~
parent: ~
@@ -369,10 +369,10 @@ expression: diagnostics
HardcodedPasswordString:
string: s3cr3t
location:
row: 53
row: 54
column: 20
end_location:
row: 53
row: 54
column: 28
fix: ~
parent: ~
@@ -380,10 +380,10 @@ expression: diagnostics
HardcodedPasswordString:
string: "1\n2"
location:
row: 55
row: 56
column: 12
end_location:
row: 55
row: 56
column: 18
fix: ~
parent: ~
@@ -391,10 +391,10 @@ expression: diagnostics
HardcodedPasswordString:
string: "3\t4"
location:
row: 58
row: 59
column: 12
end_location:
row: 58
row: 59
column: 18
fix: ~
parent: ~
@@ -402,10 +402,10 @@ expression: diagnostics
HardcodedPasswordString:
string: "5\r6"
location:
row: 61
row: 62
column: 12
end_location:
row: 61
row: 62
column: 18
fix: ~
parent: ~

View File

@@ -6,10 +6,10 @@ expression: diagnostics
HardcodedPasswordFuncArg:
string: s3cr3t
location:
row: 13
row: 14
column: 8
end_location:
row: 13
row: 14
column: 25
fix: ~
parent: ~

View File

@@ -8,17 +8,15 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::BlindExcept, Path::new("BLE.py"); "BLE001")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_blind_except")
.join(path)
.as_path(),
Path::new("flake8_blind_except").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);

View File

@@ -1,11 +1,25 @@
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers;
use crate::ast::helpers::{find_keyword, is_const_true};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
define_violation!(
pub struct BlindExcept {
pub name: String,
}
);
impl Violation for BlindExcept {
#[derive_message_formats]
fn message(&self) -> String {
let BlindExcept { name } = self;
format!("Do not catch blind exception: `{name}`")
}
}
/// BLE001
pub fn blind_except(
@@ -67,7 +81,7 @@ pub fn blind_except(
}
checker.diagnostics.push(Diagnostic::new(
violations::BlindExcept {
BlindExcept {
name: id.to_string(),
},
Range::from_located(type_),

View File

@@ -8,8 +8,8 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"); "FBT001")]
@@ -18,9 +18,7 @@ mod tests {
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_boolean_trap")
.join(path)
.as_path(),
Path::new("flake8_boolean_trap").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);

View File

@@ -10,9 +10,9 @@ mod tests {
use test_case::test_case;
use crate::assert_yaml_snapshot;
use crate::linter::test_path;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"); "B002")]
#[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"); "B003")]
@@ -45,9 +45,7 @@ mod tests {
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bugbear")
.join(path)
.as_path(),
Path::new("flake8_bugbear").join(path).as_path(),
&Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
@@ -58,7 +56,7 @@ mod tests {
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bugbear/B008_extended.py"),
Path::new("flake8_bugbear/B008_extended.py"),
&Settings {
flake8_bugbear: super::settings::Settings {
extend_immutable_calls: vec![

View File

@@ -1,10 +1,38 @@
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::{Diagnostic, Rule};
use crate::violations;
use crate::violation::Violation;
use crate::visibility::{is_abstract, is_overload};
use ruff_macros::derive_message_formats;
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
define_violation!(
pub struct AbstractBaseClassWithoutAbstractMethod {
pub name: String,
}
);
impl Violation for AbstractBaseClassWithoutAbstractMethod {
#[derive_message_formats]
fn message(&self) -> String {
let AbstractBaseClassWithoutAbstractMethod { name } = self;
format!("`{name}` is an abstract base class, but it has no abstract methods")
}
}
define_violation!(
pub struct EmptyMethodWithoutAbstractDecorator {
pub name: String,
}
);
impl Violation for EmptyMethodWithoutAbstractDecorator {
#[derive_message_formats]
fn message(&self) -> String {
let EmptyMethodWithoutAbstractDecorator { name } = self;
format!(
"`{name}` is an empty method in an abstract base class, but has no abstract decorator"
)
}
}
fn is_abc_class(checker: &Checker, bases: &[Expr], keywords: &[Keyword]) -> bool {
keywords.iter().any(|keyword| {
@@ -62,16 +90,19 @@ pub fn abstract_base_class(
continue;
}
let (StmtKind::FunctionDef {
let (
StmtKind::FunctionDef {
decorator_list,
body,
name: method_name,
..
} | StmtKind::AsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
}
| StmtKind::AsyncFunctionDef {
decorator_list,
body,
..
}) = &stmt.node else {
) = &stmt.node else {
continue;
};
@@ -88,8 +119,8 @@ pub fn abstract_base_class(
if !has_abstract_decorator && is_empty_body(body) && !is_overload(checker, decorator_list) {
checker.diagnostics.push(Diagnostic::new(
violations::EmptyMethodWithoutAbstractDecorator {
name: name.to_string(),
EmptyMethodWithoutAbstractDecorator {
name: format!("{name}.{method_name}"),
},
Range::from_located(stmt),
));
@@ -102,7 +133,7 @@ pub fn abstract_base_class(
{
if !has_abstract_method {
checker.diagnostics.push(Diagnostic::new(
violations::AbstractBaseClassWithoutAbstractMethod {
AbstractBaseClassWithoutAbstractMethod {
name: name.to_string(),
},
Range::from_located(stmt),

View File

@@ -1,11 +1,26 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind};
use crate::ast::helpers::unparse_stmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::AlwaysAutofixableViolation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind};
define_violation!(
pub struct DoNotAssertFalse;
);
impl AlwaysAutofixableViolation for DoNotAssertFalse {
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`")
}
fn autofix_title(&self) -> String {
"Replace `assert False`".to_string()
}
}
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::new(
@@ -46,7 +61,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
return;
};
let mut diagnostic = Diagnostic::new(violations::DoNotAssertFalse, Range::from_located(test));
let mut diagnostic = Diagnostic::new(DoNotAssertFalse, Range::from_located(test));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
unparse_stmt(&assertion_error(msg), checker.stylist),

View File

@@ -7,13 +7,23 @@
//! typo. Either assert for a more specific exception (builtin or
//! custom), use `assertRaisesRegex`, or use the context manager form of
//! `assertRaises`.
use rustpython_ast::{ExprKind, Stmt, Withitem};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{ExprKind, Stmt, Withitem};
define_violation!(
pub struct NoAssertRaisesException;
);
impl Violation for NoAssertRaisesException {
#[derive_message_formats]
fn message(&self) -> String {
format!("`assertRaises(Exception)` should be considered evil")
}
}
/// B017
pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) {
@@ -41,7 +51,7 @@ pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[With
}
checker.diagnostics.push(Diagnostic::new(
violations::NoAssertRaisesException,
NoAssertRaisesException,
Range::from_located(stmt),
));
}

View File

@@ -1,10 +1,20 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind};
define_violation!(
pub struct AssignmentToOsEnviron;
);
impl Violation for AssignmentToOsEnviron {
#[derive_message_formats]
fn message(&self) -> String {
format!("Assigning to `os.environ` doesn't clear the environment")
}
}
/// B003
pub fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) {
if targets.len() != 1 {
@@ -24,7 +34,7 @@ pub fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) {
return;
}
checker.diagnostics.push(Diagnostic::new(
violations::AssignmentToOsEnviron,
AssignmentToOsEnviron,
Range::from_located(target),
));
}

View File

@@ -1,9 +1,22 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{Range, ScopeKind};
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind};
define_violation!(
pub struct CachedInstanceMethod;
);
impl Violation for CachedInstanceMethod {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks"
)
}
}
fn is_cache_func(checker: &Checker, expr: &Expr) -> bool {
checker.resolve_call_path(expr).map_or(false, |call_path| {
@@ -35,7 +48,7 @@ pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) {
},
) {
checker.diagnostics.push(Diagnostic::new(
violations::CachedInstanceMethod,
CachedInstanceMethod,
Range::from_located(decorator),
));
}

View File

@@ -1,9 +1,20 @@
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, ExprKind};
define_violation!(
pub struct CannotRaiseLiteral;
);
impl Violation for CannotRaiseLiteral {
#[derive_message_formats]
fn message(&self) -> String {
format!("Cannot raise a literal. Did you intend to return it or raise an Exception?")
}
}
/// B016
pub fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) {
@@ -11,7 +22,7 @@ pub fn cannot_raise_literal(checker: &mut Checker, expr: &Expr) {
return;
};
checker.diagnostics.push(Diagnostic::new(
violations::CannotRaiseLiteral,
CannotRaiseLiteral,
Range::from_located(expr),
));
}

View File

@@ -1,14 +1,50 @@
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location};
use crate::ast::helpers;
use crate::ast::helpers::unparse_expr;
use crate::ast::types::{CallPath, Range};
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::violations;
use crate::violation::{AlwaysAutofixableViolation, Violation};
use itertools::Itertools;
use ruff_macros::derive_message_formats;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location};
define_violation!(
pub struct DuplicateTryBlockException {
pub name: String,
}
);
impl Violation for DuplicateTryBlockException {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateTryBlockException { name } = self;
format!("try-except block with duplicate exception `{name}`")
}
}
define_violation!(
pub struct DuplicateHandlerException {
pub names: Vec<String>,
}
);
impl AlwaysAutofixableViolation for DuplicateHandlerException {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateHandlerException { names } = self;
if names.len() == 1 {
let name = &names[0];
format!("Exception handler with duplicate exception: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Exception handler with duplicate exceptions: {names}")
}
}
fn autofix_title(&self) -> String {
"De-duplicate exceptions".to_string()
}
}
fn type_pattern(elts: Vec<&Expr>) -> Expr {
Expr::new(
@@ -49,7 +85,7 @@ fn duplicate_handler_exceptions<'a>(
// TODO(charlie): Handle "BaseException" and redundant exception aliases.
if !duplicates.is_empty() {
let mut diagnostic = Diagnostic::new(
violations::DuplicateHandlerException {
DuplicateHandlerException {
names: duplicates
.into_iter()
.map(|call_path| call_path.join("."))
@@ -115,7 +151,7 @@ pub fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) {
for (name, exprs) in duplicates {
for expr in exprs {
checker.diagnostics.push(Diagnostic::new(
violations::DuplicateTryBlockException {
DuplicateTryBlockException {
name: name.join("."),
},
Range::from_located(expr),

View File

@@ -1,9 +1,23 @@
use rustpython_ast::{ExprKind, Stmt, StmtKind};
use crate::ast::helpers;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{ExprKind, Stmt, StmtKind};
define_violation!(
pub struct FStringDocstring;
);
impl Violation for FStringDocstring {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"f-string used as docstring. This will be interpreted by python as a joined string \
rather than a docstring."
)
}
}
/// B021
pub fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
@@ -17,7 +31,7 @@ pub fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
return;
};
checker.diagnostics.push(Diagnostic::new(
violations::FStringDocstring,
FStringDocstring,
helpers::identifier_range(stmt, checker.locator),
));
}

View File

@@ -1,13 +1,31 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
use super::mutable_argument_default::is_mutable_func;
use crate::ast::helpers::{compose_call_path, to_call_path};
use crate::ast::types::{CallPath, Range};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::{Diagnostic, DiagnosticKind};
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Arguments, Constant, Expr, ExprKind};
define_violation!(
pub struct FunctionCallArgumentDefault {
pub name: Option<String>,
}
);
impl Violation for FunctionCallArgumentDefault {
#[derive_message_formats]
fn message(&self) -> String {
let FunctionCallArgumentDefault { name } = self;
if let Some(name) = name {
format!("Do not perform function call `{name}` in argument defaults")
} else {
format!("Do not perform function call in argument defaults")
}
}
}
const IMMUTABLE_FUNCS: &[&[&str]] = &[
&["", "tuple"],
@@ -48,7 +66,7 @@ where
&& !is_nan_or_infinity(func, args)
{
self.diagnostics.push((
violations::FunctionCallArgumentDefault {
FunctionCallArgumentDefault {
name: compose_call_path(expr),
}
.into(),

View File

@@ -1,13 +1,27 @@
use rustc_hash::FxHashSet;
use rustpython_ast::{Comprehension, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::collect_arg_names;
use crate::ast::types::{Node, Range};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
use rustc_hash::FxHashSet;
use rustpython_ast::{Comprehension, Expr, ExprContext, ExprKind, Stmt, StmtKind};
define_violation!(
pub struct FunctionUsesLoopVariable {
pub name: String,
}
);
impl Violation for FunctionUsesLoopVariable {
#[derive_message_formats]
fn message(&self) -> String {
let FunctionUsesLoopVariable { name } = self;
format!("Function definition does not bind loop variable `{name}`")
}
}
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
@@ -260,7 +274,7 @@ where
if !checker.flake8_bugbear_seen.contains(&expr) {
checker.flake8_bugbear_seen.push(expr);
checker.diagnostics.push(Diagnostic::new(
violations::FunctionUsesLoopVariable {
FunctionUsesLoopVariable {
name: name.to_string(),
},
range,

View File

@@ -1,14 +1,31 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location};
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::fix::Fix;
use crate::python::identifiers::{is_identifier, is_mangled_private};
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::violations;
use crate::violation::AlwaysAutofixableViolation;
use ruff_macros::derive_message_formats;
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location};
define_violation!(
pub struct GetAttrWithConstant;
);
impl AlwaysAutofixableViolation for GetAttrWithConstant {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Do not call `getattr` with a constant attribute value. It is not any safer than \
normal property access."
)
}
fn autofix_title(&self) -> String {
"Replace `getattr` with attribute access".to_string()
}
}
fn attribute(value: &Expr, attr: &str) -> Expr {
Expr::new(
Location::default(),
@@ -45,8 +62,7 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
return;
}
let mut diagnostic =
Diagnostic::new(violations::GetAttrWithConstant, Range::from_located(expr));
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(

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