Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
8fbec8e6a2 Update docs to match updated logo and color palette 2023-06-21 20:48:29 -04:00
752 changed files with 10900 additions and 38444 deletions

1
.gitattributes vendored
View File

@@ -4,4 +4,3 @@ crates/ruff/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

View File

@@ -31,6 +31,17 @@ jobs:
cargo-clippy:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
run: |
rustup component add clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo-clippy-wasm:
name: "cargo clippy (wasm)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
@@ -38,10 +49,7 @@ jobs:
rustup component add clippy
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
- run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
cargo-test:
strategy:

View File

@@ -491,16 +491,16 @@ jobs:
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:
draft: true
draft: false
files: binaries/*
tag_name: v${{ inputs.tag }}
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Update dependents
name: Release
runs-on: ubuntu-latest
needs: publish-release
needs: release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v6

View File

@@ -6,9 +6,7 @@ exclude: |
crates/ruff/src/rules/.*/snapshots/.*|
crates/ruff_cli/resources/.*|
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/tests/snapshots/.*|
crates/ruff_python_resolver/resources/.*|
crates/ruff_python_resolver/tests/snapshots/.*
crates/ruff_python_formatter/src/snapshots/.*
)$
repos:

View File

@@ -1,67 +1,5 @@
# Breaking Changes
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))
Ruff maintains a list of default exclusions, which now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git`
- `.git-rewrite`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `build`
- `dist`
- `node_modules`
- `venv`
Previously, the `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` directories were not
excluded by default. This change brings Ruff's default exclusions in line with other tools like
Black.
## 0.0.276
### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470))
The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was
removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring
the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism.
Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as
follows:
- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore
`UP006` violations, even if `from __future__ import annotations` is present in the file.
While such annotations are valid in Python 3.7 and Python 3.8 when combined with
`from __future__ import annotations`, they aren't supported by libraries like Pydantic and
FastAPI, which rely on runtime type checking.
- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation,
and libraries like Pydantic and FastAPI support it without issue.
In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations
that are not supported at runtime by the current Python version, which are unsupported by libraries
like Pydantic and FastAPI.
Note that this is not a breaking change, but is included here to complement the previous removal
of `keep-runtime-typing`.
## 0.0.268
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427))

View File

@@ -327,18 +327,22 @@ git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resour
To benchmark the release build:
```shell
cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache -e" \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ -e"
cargo build --release && hyperfine --ignore-failure --warmup 10 \
"./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache" \
"./target/release/ruff ./crates/ruff/resources/test/cpython/"
Benchmark 1: ./target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache
Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms]
Range (min … max): 289.9 ms … 301.6 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: ./target/release/ruff ./crates/ruff/resources/test/cpython/
Time (mean ± σ): 48.0 ms ± 3.1 ms [User: 65.2 ms, System: 124.7 ms]
Range (min … max): 45.0 ms … 66.7 ms 62 runs
Warning: Ignoring non-zero exit code.
Summary
'./target/release/ruff ./crates/ruff/resources/test/cpython/' ran
6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff/resources/test/cpython/ --no-cache'
@@ -499,7 +503,7 @@ examples.
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to

436
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.30.0" }
insta = { version = "1.28.0" }
is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
@@ -49,15 +49,16 @@ toml = { version = "0.7.2" }
# v0.0.1
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
# Please tag the RustPython version everytime you update its revision here and in fuzz/Cargo.toml
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
# Current tag: v0.0.7
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "c174bbf1f29527edd43d432326327f16f47ab9e0" , default-features = false, features = ["full-lexer", "num-bigint"] }
# v0.0.3
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" }
# v0.0.3
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
# v0.0.3
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false, features = ["num-bigint"] }
# v0.0.3
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false }
# v0.0.3
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
[profile.release]
lto = "fat"

26
LICENSE
View File

@@ -1224,32 +1224,6 @@ are:
SOFTWARE.
"""
- Pyright, licensed as follows:
"""
MIT License
Pyright - A static type checker for the Python language
Copyright (c) Microsoft Corporation. All rights reserved.
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
"""
- rust-analyzer/text-size, licensed under the MIT license:
"""
Permission is hereby granted, free of charge, to any

View File

@@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.277
rev: v0.0.274
hooks:
- id: ruff
```
@@ -330,11 +330,9 @@ We're grateful to the maintainers of these tools for their work, and for all
the value they've provided to the Python community.
Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter),
and again draws on both API and implementation details from [Rome](https://github.com/rome/tools),
and again draws on both the APIs and implementation details of [Rome](https://github.com/rome/tools),
[Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black).
Ruff's import resolver is based on the import resolution algorithm from [Pyright](https://github.com/microsoft/pyright).
Ruff is also influenced by a number of tools outside the Python ecosystem, like
[Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint).

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.277"
version = "0.0.274"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.277"
version = "0.0.274"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -17,7 +17,6 @@ name = "ruff"
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_whitespace = { path = "../ruff_python_whitespace" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
@@ -43,7 +42,6 @@ is-macro = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
nohash-hasher = { workspace = true }
num-bigint = { workspace = true }
@@ -89,5 +87,4 @@ colored = { workspace = true, features = ["no-color"] }
[features]
default = []
schemars = ["dep:schemars"]
# Enables the UnreachableCode rule
unreachable-code = []
jupyter_notebook = []

View File

@@ -1,11 +0,0 @@
def func():
assert True
def func():
assert False
def func():
assert True, "oops"
def func():
assert False, "oops"

View File

@@ -1,41 +0,0 @@
def func():
async for i in range(5):
print(i)
def func():
async for i in range(20):
print(i)
else:
return 0
def func():
async for i in range(10):
if i == 5:
return 1
return 0
def func():
async for i in range(111):
if i == 5:
return 1
else:
return 0
return 2
def func():
async for i in range(12):
continue
def func():
async for i in range(1110):
if True:
continue
def func():
async for i in range(13):
break
def func():
async for i in range(1110):
if True:
break

View File

@@ -1,41 +0,0 @@
def func():
for i in range(5):
print(i)
def func():
for i in range(20):
print(i)
else:
return 0
def func():
for i in range(10):
if i == 5:
return 1
return 0
def func():
for i in range(111):
if i == 5:
return 1
else:
return 0
return 2
def func():
for i in range(12):
continue
def func():
for i in range(1110):
if True:
continue
def func():
for i in range(13):
break
def func():
for i in range(1110):
if True:
break

View File

@@ -1,108 +0,0 @@
def func():
if False:
return 0
return 1
def func():
if True:
return 1
return 0
def func():
if False:
return 0
else:
return 1
def func():
if True:
return 1
else:
return 0
def func():
if False:
return 0
else:
return 1
return "unreachable"
def func():
if True:
return 1
else:
return 0
return "unreachable"
def func():
if True:
if True:
return 1
return 2
else:
return 3
return "unreachable2"
def func():
if False:
return 0
def func():
if True:
return 1
def func():
if True:
return 1
elif False:
return 2
else:
return 0
def func():
if False:
return 1
elif True:
return 2
else:
return 0
def func():
if True:
if False:
return 0
elif True:
return 1
else:
return 2
return 3
elif True:
return 4
else:
return 5
return 6
def func():
if False:
return "unreached"
elif False:
return "also unreached"
return "reached"
# Test case found in the Bokeh repository that trigger a false positive.
def func(self, obj: BytesRep) -> bytes:
data = obj["data"]
if isinstance(data, str):
return base64.b64decode(data)
elif isinstance(data, Buffer):
buffer = data
else:
id = data["id"]
if id in self._buffers:
buffer = self._buffers[id]
else:
self.error(f"can't resolve buffer '{id}'")
return buffer.data

View File

@@ -1,131 +0,0 @@
def func(status):
match status:
case _:
return 0
return "unreachable"
def func(status):
match status:
case 1:
return 1
return 0
def func(status):
match status:
case 1:
return 1
case _:
return 0
def func(status):
match status:
case 1 | 2 | 3:
return 5
return 6
def func(status):
match status:
case 1 | 2 | 3:
return 5
case _:
return 10
return 0
def func(status):
match status:
case 0:
return 0
case 1:
return 1
case 1:
return "1 again"
case _:
return 3
def func(status):
i = 0
match status, i:
case _, _:
return 0
def func(status):
i = 0
match status, i:
case _, 0:
return 0
case _, 2:
return 0
def func(point):
match point:
case (0, 0):
print("Origin")
case _:
raise ValueError("oops")
def func(point):
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")
def where_is(point):
class Point:
x: int
y: int
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=0, y=y):
print(f"Y={y}")
case Point(x=x, y=0):
print(f"X={x}")
case Point():
print("Somewhere else")
case _:
print("Not a point")
def func(points):
match points:
case []:
print("No points")
case [Point(0, 0)]:
print("The origin")
case [Point(x, y)]:
print(f"Single point {x}, {y}")
case [Point(0, y1), Point(0, y2)]:
print(f"Two on the Y axis at {y1}, {y2}")
case _:
print("Something else")
def func(point):
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")
def func():
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
match color:
case Color.RED:
print("I see red!")
case Color.GREEN:
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")

View File

@@ -1,5 +0,0 @@
def func():
raise Exception
def func():
raise "a glass!"

View File

@@ -1,23 +0,0 @@
def func():
pass
def func():
pass
def func():
return
def func():
return 1
def func():
return 1
return "unreachable"
def func():
i = 0
def func():
i = 0
i += 2
return i

View File

@@ -1,41 +0,0 @@
def func():
try:
...
except Exception:
...
except OtherException as e:
...
else:
...
finally:
...
def func():
try:
...
except Exception:
...
def func():
try:
...
except Exception:
...
except OtherException as e:
...
def func():
try:
...
except Exception:
...
except OtherException as e:
...
else:
...
def func():
try:
...
finally:
...

View File

@@ -1,121 +0,0 @@
def func():
while False:
return "unreachable"
return 1
def func():
while False:
return "unreachable"
else:
return 1
def func():
while False:
return "unreachable"
else:
return 1
return "also unreachable"
def func():
while True:
return 1
return "unreachable"
def func():
while True:
return 1
else:
return "unreachable"
def func():
while True:
return 1
else:
return "unreachable"
return "also unreachable"
def func():
i = 0
while False:
i += 1
return i
def func():
i = 0
while True:
i += 1
return i
def func():
while True:
pass
return 1
def func():
i = 0
while True:
if True:
print("ok")
i += 1
return i
def func():
i = 0
while True:
if False:
print("ok")
i += 1
return i
def func():
while True:
if True:
return 1
return 0
def func():
while True:
continue
def func():
while False:
continue
def func():
while True:
break
def func():
while False:
break
def func():
while True:
if True:
continue
def func():
while True:
if True:
break
'''
TODO: because `try` statements aren't handled this triggers a false positive as
the last statement is reached, but the rules thinks it isn't (it doesn't
see/process the break statement).
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
self.stop_serving = False
while True:
try:
self.server = HTTPServer((host, port), HtmlOnlyHandler)
self.host = host
self.port = port
break
except OSError:
log.debug(f"port {port} is in use, trying to next one")
port += 1
self.thread = threading.Thread(target=self._run_web_server)
'''

View File

@@ -1,12 +0,0 @@
import os
print(eval("1+1")) # S307
print(eval("os.getcwd()")) # S307
class Class(object):
def eval(self):
print("hi")
def foo(self):
self.eval() # OK

View File

@@ -23,10 +23,6 @@ class Foobar(unittest.TestCase):
with self.assertRaises(Exception):
raise Exception("Evil I say!")
def also_evil_raises(self) -> None:
with self.assertRaises(BaseException):
raise Exception("Evil I say!")
def context_manager_raises(self) -> None:
with self.assertRaises(Exception) as ex:
raise Exception("Context manager is good")
@@ -45,9 +41,6 @@ def test_pytest_raises():
with pytest.raises(Exception):
raise ValueError("Hello")
with pytest.raises(Exception), pytest.raises(ValueError):
raise ValueError("Hello")
with pytest.raises(Exception, "hello"):
raise ValueError("This is fine")

View File

@@ -111,19 +111,3 @@ class PerfectlyFine(models.Model):
@property
def random_property(self):
return "%s" % self
class MultipleConsecutiveFields(models.Model):
"""Model that contains multiple out-of-order field definitions in a row."""
class Meta:
verbose_name = "test"
first_name = models.CharField(max_length=32)
last_name = models.CharField(max_length=32)
def get_absolute_url(self):
pass
middle_name = models.CharField(max_length=32)

View File

@@ -1,6 +0,0 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -1,6 +0,0 @@
import sys
if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info
if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -1,31 +0,0 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -1,31 +0,0 @@
import sys
if sys.version_info[0] == 2: ...
if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:'
if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check
if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2,): ...
if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple
if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check
if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check
if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check
if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check
if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version
if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons
if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons
if sys.version_info < (3, 5): ...
if sys.version_info >= (3, 5): ...
if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info

View File

@@ -1,15 +0,0 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@@ -1,15 +0,0 @@
import sys
from sys import version_info
if sys.version_info >= (3, 4, 3): ... # PYI004
if sys.version_info < (3, 4, 3): ... # PYI004
if sys.version_info == (3, 4, 3): ... # PYI004
if sys.version_info != (3, 4, 3): ... # PYI004
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if sys.platform == 'linux': ...

View File

@@ -1,14 +0,0 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@@ -1,14 +0,0 @@
import sys
from sys import platform, version_info
if sys.version_info[:1] == (2, 7): ... # Y005
if sys.version_info[:2] == (2,): ... # Y005
if sys.version_info[0] == 2: ...
if version_info[0] == 2: ...
if sys.version_info < (3, 5): ...
if version_info >= (3, 5): ...
if sys.version_info[:2] == (2, 7): ...
if sys.version_info[:1] == (2,): ...
if platform == 'linux': ...

View File

@@ -91,4 +91,3 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@@ -98,4 +98,3 @@ field27 = list[str]
field28 = builtins.str
field29 = str
field30 = str | bytes | None
field31: typing.Final = field30

View File

@@ -36,11 +36,3 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg"
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg"
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff"
class Demo:
"""Docstrings are excluded from this rule. Some padding."""
def func() -> None:
"""Docstrings are excluded from this rule. Some padding."""

View File

@@ -28,9 +28,3 @@ bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI05
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
class Demo:
"""Docstrings are excluded from this rule. Some padding.""" # OK
def func() -> None:
"""Docstrings are excluded from this rule. Some padding.""" # OK

View File

@@ -1,12 +1,6 @@
# T002 - accepted
# TODO (evanrittenhouse): this has an author
# TODO(evanrittenhouse): this has an author
# TODO (evanrittenhouse) and more: this has an author
# TODO(evanrittenhouse) and more: this has an author
# TODO@mayrholu: this has an author
# TODO @mayrholu: this has an author
# TODO@mayrholu and more: this has an author
# TODO @mayrholu and more: this has an author
# TODO(evanrittenhouse): this also has an author
# T002 - errors
# TODO: this has no author
# FIXME: neither does this

View File

@@ -1,6 +1,5 @@
# Do this (new version)
from numpy.random import default_rng
rng = default_rng()
vals = rng.standard_normal(10)
more_vals = rng.standard_normal(10)
@@ -8,13 +7,11 @@ numbers = rng.integers(high, size=5)
# instead of this (legacy version)
from numpy import random
vals = random.standard_normal(10)
more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5)
import numpy
numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()

View File

@@ -1,15 +0,0 @@
import numpy as np
np.round_(np.random.rand(5, 5), 2)
np.product(np.random.rand(5, 5))
np.cumproduct(np.random.rand(5, 5))
np.sometrue(np.random.rand(5, 5))
np.alltrue(np.random.rand(5, 5))
from numpy import round_, product, cumproduct, sometrue, alltrue
round_(np.random.rand(5, 5), 2)
product(np.random.rand(5, 5))
cumproduct(np.random.rand(5, 5))
sometrue(np.random.rand(5, 5))
alltrue(np.random.rand(5, 5))

View File

@@ -4,9 +4,7 @@ x = pd.DataFrame()
x.drop(["a"], axis=1, inplace=True)
x.y.drop(["a"], axis=1, inplace=True)
x["y"].drop(["a"], axis=1, inplace=True)
x.drop(["a"], axis=1, inplace=True)
x.drop(
inplace=True,
@@ -25,7 +23,6 @@ x.drop(["a"], axis=1, **kwargs, inplace=True)
x.drop(["a"], axis=1, inplace=True, **kwargs)
f(x.drop(["a"], axis=1, inplace=True))
x.apply(lambda x: x.sort_values("a", inplace=True))
x.apply(lambda x: x.sort_values('a', inplace=True))
import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call

View File

@@ -1,4 +1,4 @@
import abc
from abc import ABCMeta
import pydantic
@@ -19,10 +19,6 @@ class Class:
def class_method(cls):
pass
@abc.abstractclassmethod
def abstract_class_method(cls):
pass
@staticmethod
def static_method(x):
return x
@@ -45,7 +41,7 @@ class Class:
...
class MetaClass(abc.ABCMeta):
class MetaClass(ABCMeta):
def bad_method(self):
pass

View File

@@ -1,4 +1,4 @@
import abc
from abc import ABCMeta
import pydantic
@@ -34,23 +34,6 @@ class Class:
def stillBad(cls, my_field: str) -> str:
pass
@classmethod
def badAllowed(cls):
pass
@classmethod
def stillBad(cls):
pass
@abc.abstractclassmethod
def badAllowed(cls):
pass
@abc.abstractclassmethod
def stillBad(cls):
pass
class PosOnlyClass:
def badAllowed(this, blah, /, self, something: str):
pass

View File

@@ -1,52 +0,0 @@
foo_tuple = (1, 2, 3)
foo_list = [1, 2, 3]
foo_set = {1, 2, 3}
foo_dict = {1: 2, 3: 4}
foo_int = 123
for i in list(foo_tuple): # PERF101
pass
for i in list(foo_list): # PERF101
pass
for i in list(foo_set): # PERF101
pass
for i in list((1, 2, 3)): # PERF101
pass
for i in list([1, 2, 3]): # PERF101
pass
for i in list({1, 2, 3}): # PERF101
pass
for i in list(
{
1,
2,
3,
}
):
pass
for i in list( # Comment
{1, 2, 3}
): # PERF101
pass
for i in list(foo_dict): # Ok
pass
for i in list(1): # Ok
pass
for i in list(foo_int): # Ok
pass
import itertools
for i in itertools.product(foo_int): # Ok
pass

View File

@@ -1,28 +0,0 @@
for i in range(10):
try: # PERF203
print(f"{i}")
except:
print("error")
try:
for i in range(10):
print(f"{i}")
except:
print("error")
i = 0
while i < 10: # PERF203
try:
print(f"{i}")
except:
print("error")
i += 1
try:
i = 0
while i < 10:
print(f"{i}")
i += 1
except:
print("error")

View File

@@ -1,39 +0,0 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
elif i % 2:
result.append(i) # PERF401
else:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # OK
def f():
items = [1, 2, 3, 4]
result = {}
for i in items:
result[i].append(i) # OK

View File

@@ -1,26 +0,0 @@
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.insert(0, i) # PERF402
def f():
items = [1, 2, 3, 4]
result = []
for i in items:
result.append(i * i) # OK
def f():
items = [1, 2, 3, 4]
result = {}
for i in items:
result[i].append(i * i) # OK

View File

@@ -1,135 +1,51 @@
def scope():
# E731
#: E731
f = lambda x: 2 * x
#: E731
f = lambda x: 2 * x
#: E731
while False:
this = lambda y, z: 2 * x
#: E731
f = lambda: (yield 1)
#: E731
f = lambda: (yield from g())
#: E731
class F:
f = lambda x: 2 * x
def scope():
# E731
f = lambda x: 2 * x
f = object()
f.method = lambda: "Method"
f = {}
f["a"] = lambda x: x**2
f = []
f.append(lambda x: x**2)
f = g = lambda x: x**2
lambda: "no-op"
# Annotated
from typing import Callable, ParamSpec
def scope():
# E731
while False:
this = lambda y, z: 2 * x
def scope():
# E731
f = lambda: (yield 1)
def scope():
# E731
f = lambda: (yield from g())
def scope():
# OK
f = object()
f.method = lambda: "Method"
def scope():
# OK
f = {}
f["a"] = lambda x: x**2
def scope():
# OK
f = []
f.append(lambda x: x**2)
def scope():
# OK
f = g = lambda x: x**2
def scope():
# OK
lambda: "no-op"
class Scope:
# E731
f = lambda x: 2 * x
class Scope:
from typing import Callable
# E731
f: Callable[[int], int] = lambda x: 2 * x
def scope():
# E731
from typing import Callable
x: Callable[[int], int]
if True:
x = lambda: 1
else:
x = lambda: 2
return x
def scope():
# E731
from typing import Callable, ParamSpec
# ParamSpec cannot be used in this context, so do not preserve the annotation.
P = ParamSpec("P")
f: Callable[P, int] = lambda *args: len(args)
def scope():
# E731
from typing import Callable
f: Callable[[], None] = lambda: None
def scope():
# E731
from typing import Callable
f: Callable[..., None] = lambda a, b: None
def scope():
# E731
from typing import Callable
f: Callable[[int], int] = lambda x: 2 * x
P = ParamSpec("P")
# ParamSpec cannot be used in this context, so do not preserve the annotation.
f: Callable[P, int] = lambda *args: len(args)
f: Callable[[], None] = lambda: None
f: Callable[..., None] = lambda a, b: None
f: Callable[[int], int] = lambda x: 2 * x
# Let's use the `Callable` type from `collections.abc` instead.
def scope():
# E731
from collections.abc import Callable
from collections.abc import Callable
f: Callable[[str, int], str] = lambda a, b: a * b
f: Callable[[str, int], str] = lambda a, b: a * b
f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
def scope():
# E731
from collections.abc import Callable
f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
# Override `Callable`
class Callable:
pass
def scope():
# E731
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
# Do not copy the annotation from here on out.
f: Callable[[str, int], str] = lambda a, b: a * b

View File

@@ -19,14 +19,6 @@ with \_ somewhere
in the middle
"""
#: W605:1:38
value = 'new line\nand invalid escape \_ here'
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'

View File

@@ -19,11 +19,6 @@ with \_ somewhere
in the middle
"""
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'

View File

@@ -1,29 +0,0 @@
def double_quotes_backslash():
"""Sum\\mary."""
def double_quotes_backslash_raw():
r"""Sum\mary."""
def double_quotes_backslash_uppercase():
R"""Sum\\mary."""
def make_unique_pod_id(pod_id: str) -> str | None:
r"""
Generate a unique Pod name.
Kubernetes pod names must consist of one or more lowercase
rfc1035/rfc1123 labels separated by '.' with a maximum length of 253
characters.
Name must pass the following regex for validation
``^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$``
For more details, see:
https://github.com/kubernetes/kubernetes/blob/release-1.1/docs/design/identifiers.md
:param pod_id: requested pod name
:return: ``str`` valid Pod name of appropriate length
"""

View File

@@ -1,25 +0,0 @@
def f(a: int, b: int) -> int:
"""Showcase function.
Parameters
----------
a : int
_description_
b : int
_description_
Returns
-------
int
_description
"""
return b - a
def f() -> int:
"""Showcase function.
Parameters
----------
Returns
-------
"""

View File

@@ -513,19 +513,3 @@ def implicit_string_concatenation():
A value of some sort.
""""Extra content"
def replace_equals_with_dash():
"""Equal length equals should be replaced with dashes.
Parameters
==========
"""
def replace_equals_with_dash2():
"""Here, the length of equals is not the same.
Parameters
===========
"""

View File

@@ -37,10 +37,7 @@ f"{{test}}"
f'{{ 40 }}'
f"{{a {{x}}"
f"{{{{x}}}}"
""f""
''f""
(""f""r"")
# To be fixed
# Error: f-string: single '}' is not allowed at line 41 column 8
# f"\{{x}}"
# f"\{{x}}"

View File

@@ -36,6 +36,3 @@ for item in set(("apples", "lemons", "water")): # set constructor is fine
for number in {i for i in range(10)}: # set comprehensions are fine
print(number)
for item in {*numbers_set, 4, 5, 6}: # set unpacking is fine
print(f"I like {item}.")

View File

@@ -1,35 +0,0 @@
# Errors.
class Foo:
__slots__ = "bar"
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: str = "bar"
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: str = f"bar"
def __init__(self, bar):
self.bar = bar
# Non-errors.
class Foo:
__slots__ = ("bar",)
def __init__(self, bar):
self.bar = bar
class Foo:
__slots__: tuple[str, ...] = ("bar",)
def __init__(self, bar):
self.bar = bar

View File

@@ -1,56 +0,0 @@
from typing import TypeVar, ParamSpec, NewType, TypeVarTuple
# Errors.
X = TypeVar("T")
X = TypeVar(name="T")
Y = ParamSpec("T")
Y = ParamSpec(name="T")
Z = NewType("T", int)
Z = NewType(name="T", tp=int)
Ws = TypeVarTuple("Ts")
Ws = TypeVarTuple(name="Ts")
# Non-errors.
T = TypeVar("T")
T = TypeVar(name="T")
T = ParamSpec("T")
T = ParamSpec(name="T")
T = NewType("T", int)
T = NewType(name="T", tp=int)
Ts = TypeVarTuple("Ts")
Ts = TypeVarTuple(name="Ts")
# Errors, but not covered by this rule.
# Non-string literal name.
T = TypeVar(some_str)
T = TypeVar(name=some_str)
T = TypeVar(1)
T = TypeVar(name=1)
T = ParamSpec(some_str)
T = ParamSpec(name=some_str)
T = ParamSpec(1)
T = ParamSpec(name=1)
T = NewType(some_str, int)
T = NewType(name=some_str, tp=int)
T = NewType(1, int)
T = NewType(name=1, tp=int)
Ts = TypeVarTuple(some_str)
Ts = TypeVarTuple(name=some_str)
Ts = TypeVarTuple(1)
Ts = TypeVarTuple(name=1)
# No names provided.
T = TypeVar()
T = ParamSpec()
T = NewType()
T = NewType(tp=int)
Ts = TypeVarTuple()

View File

@@ -70,8 +70,3 @@ print("foo".encode()) # print(b"foo")
"abc"
"def"
)).encode()
(f"foo{bar}").encode("utf-8")
(f"foo{bar}").encode(encoding="utf-8")
("unicode text©").encode("utf-8")
("unicode text©").encode(encoding="utf-8")

View File

@@ -48,12 +48,3 @@ if True: from collections import (
# OK
from a import b
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import SupportsIndex
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import NamedTuple
# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
from typing_extensions import dataclass_transform

View File

@@ -1,14 +1,23 @@
import typing
from typing import ClassVar, Sequence, Final
KNOWINGLY_MUTABLE_DEFAULT = []
class A:
__slots__ = {
"mutable_default": "A mutable default value",
}
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: typing.ClassVar[list[int]] = []
final_variable: typing.Final[list[int]] = []
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
@@ -21,6 +30,7 @@ class C:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
@@ -33,5 +43,7 @@ class D(BaseModel):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -221,23 +221,3 @@ def f(arg: Union["No" "ne", "int"] = None):
# Avoid flagging when there's a parse error in the forward reference
def f(arg: Union["<>", "int"] = None):
pass
# Type aliases
Text = str | bytes
def f(arg: Text = None):
pass
def f(arg: "Text" = None):
pass
from custom_typing import MaybeInt
def f(arg: MaybeInt = None):
pass

View File

@@ -1,185 +0,0 @@
def after_return():
return "reachable"
return "unreachable"
async def also_works_on_async_functions():
return "reachable"
return "unreachable"
def if_always_true():
if True:
return "reachable"
return "unreachable"
def if_always_false():
if False:
return "unreachable"
return "reachable"
def if_elif_always_false():
if False:
return "unreachable"
elif False:
return "also unreachable"
return "reachable"
def if_elif_always_true():
if False:
return "unreachable"
elif True:
return "reachable"
return "also unreachable"
def ends_with_if():
if False:
return "unreachable"
else:
return "reachable"
def infinite_loop():
while True:
continue
return "unreachable"
''' TODO: we could determine these, but we don't yet.
def for_range_return():
for i in range(10):
if i == 5:
return "reachable"
return "unreachable"
def for_range_else():
for i in range(111):
if i == 5:
return "reachable"
else:
return "unreachable"
return "also unreachable"
def for_range_break():
for i in range(13):
return "reachable"
return "unreachable"
def for_range_if_break():
for i in range(1110):
if True:
return "reachable"
return "unreachable"
'''
def match_wildcard(status):
match status:
case _:
return "reachable"
return "unreachable"
def match_case_and_wildcard(status):
match status:
case 1:
return "reachable"
case _:
return "reachable"
return "unreachable"
def raise_exception():
raise Exception
return "unreachable"
def while_false():
while False:
return "unreachable"
return "reachable"
def while_false_else():
while False:
return "unreachable"
else:
return "reachable"
def while_false_else_return():
while False:
return "unreachable"
else:
return "reachable"
return "also unreachable"
def while_true():
while True:
return "reachable"
return "unreachable"
def while_true_else():
while True:
return "reachable"
else:
return "unreachable"
def while_true_else_return():
while True:
return "reachable"
else:
return "unreachable"
return "also unreachable"
def while_false_var_i():
i = 0
while False:
i += 1
return i
def while_true_var_i():
i = 0
while True:
i += 1
return i
def while_infinite():
while True:
pass
return "unreachable"
def while_if_true():
while True:
if True:
return "reachable"
return "unreachable"
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh1(self, obj: BytesRep) -> bytes:
data = obj["data"]
if isinstance(data, str):
return base64.b64decode(data)
elif isinstance(data, Buffer):
buffer = data
else:
id = data["id"]
if id in self._buffers:
buffer = self._buffers[id]
else:
self.error(f"can't resolve buffer '{id}'")
return buffer.data
'''
TODO: because `try` statements aren't handled this triggers a false positive as
the last statement is reached, but the rules thinks it isn't (it doesn't
see/process the break statement).
# Test case found in the Bokeh repository that trigger a false positive.
def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
self.stop_serving = False
while True:
try:
self.server = HTTPServer((host, port), HtmlOnlyHandler)
self.host = host
self.port = port
break
except OSError:
log.debug(f"port {port} is in use, trying to next one")
port += 1
self.thread = threading.Thread(target=self._run_web_server)
'''

View File

@@ -36,7 +36,6 @@ use crate::importer::Importer;
use crate::noqa::NoqaMapping;
use crate::registry::Rule;
use crate::rules::flake8_builtins::helpers::AnyShadowing;
use crate::rules::{
airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
@@ -72,7 +71,7 @@ pub(crate) struct Checker<'a> {
deferred: Deferred<'a>,
pub(crate) diagnostics: Vec<Diagnostic>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>,
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
}
impl<'a> Checker<'a> {
@@ -359,7 +358,11 @@ where
..
}) => {
if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(self, decorator_list);
self.diagnostics
.extend(flake8_django::rules::non_leading_receiver_decorator(
decorator_list,
|expr| self.semantic.resolve_call_path(expr),
));
}
if self.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) =
@@ -501,7 +504,8 @@ where
}
}
if self.enabled(Rule::HardcodedPasswordDefault) {
flake8_bandit::rules::hardcoded_password_default(self, args);
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_default(args));
}
if self.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(self, stmt, decorator_list, args);
@@ -619,11 +623,6 @@ where
);
}
}
#[cfg(feature = "unreachable-code")]
if self.enabled(Rule::UnreachableCode) {
self.diagnostics
.extend(ruff::rules::unreachable::in_function(name, body));
}
}
Stmt::Return(_) => {
if self.enabled(Rule::ReturnOutsideFunction) {
@@ -644,7 +643,10 @@ where
},
) => {
if self.enabled(Rule::DjangoNullableModelStringField) {
flake8_django::rules::nullable_model_string_field(self, body);
self.diagnostics
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
}
if self.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) =
@@ -665,17 +667,21 @@ where
}
if !self.is_stub {
if self.enabled(Rule::DjangoModelWithoutDunderStr) {
flake8_django::rules::model_without_dunder_str(self, class_def);
if let Some(diagnostic) =
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
}
}
if self.enabled(Rule::GlobalStatement) {
pylint::rules::global_statement(self, name);
}
if self.enabled(Rule::UselessObjectInheritance) {
pyupgrade::rules::useless_object_inheritance(self, class_def);
pyupgrade::rules::useless_object_inheritance(self, class_def, stmt);
}
if self.enabled(Rule::UnnecessaryClassParentheses) {
pyupgrade::rules::unnecessary_class_parentheses(self, class_def);
pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt);
}
if self.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) =
@@ -764,9 +770,6 @@ where
if self.enabled(Rule::NoSlotsInNamedtupleSubclass) {
flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def);
}
if self.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(self, class_def);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if self.enabled(Rule::MultipleImportsOnOneLine) {
@@ -1364,51 +1367,6 @@ where
self.diagnostics.push(diagnostic);
}
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
Rule::WrongTupleLengthVersionComparison,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_version_info(self, value);
}
} else {
flake8_pyi::rules::unrecognized_version_info(self, test);
}
}
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::unrecognized_platform(self, value);
}
} else {
flake8_pyi::rules::unrecognized_platform(self, test);
}
}
if self.enabled(Rule::BadVersionInfoComparison) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::bad_version_info_comparison(self, value);
}
} else {
flake8_pyi::rules::bad_version_info_comparison(self, test);
}
}
if self.enabled(Rule::ComplexIfStatementInStub) {
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
for value in values {
flake8_pyi::rules::complex_if_statement_in_stub(self, value);
}
} else {
flake8_pyi::rules::complex_if_statement_in_stub(self, test);
}
}
}
}
Stmt::Assert(ast::StmtAssert {
test,
@@ -1448,7 +1406,7 @@ where
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if self.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(self, items);
flake8_bugbear::rules::assert_raises_exception(self, stmt, items);
}
if self.enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(self, stmt, items, body);
@@ -1472,9 +1430,6 @@ where
if self.enabled(Rule::UselessElseOnLoop) {
pylint::rules::useless_else_on_loop(self, stmt, body, orelse);
}
if self.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(self, body);
}
}
Stmt::For(ast::StmtFor {
target,
@@ -1522,22 +1477,10 @@ where
if self.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(self, target, iter);
}
if self.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(self, body);
}
}
if self.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(self, target, iter);
}
if self.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(self, target, body);
}
if self.enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(self, target, body);
}
if self.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(self, iter);
}
}
Stmt::Try(ast::StmtTry {
body,
@@ -1573,7 +1516,9 @@ where
pyupgrade::rules::os_error_alias_handlers(self, handlers);
}
if self.enabled(Rule::PytestAssertInExcept) {
flake8_pytest_style::rules::assert_in_exception_handler(self, handlers);
self.diagnostics.extend(
flake8_pytest_style::rules::assert_in_exception_handler(handlers),
);
}
if self.enabled(Rule::SuppressibleException) {
flake8_simplify::rules::suppressible_exception(
@@ -1614,7 +1559,11 @@ where
flake8_bugbear::rules::assignment_to_os_environ(self, targets);
}
if self.enabled(Rule::HardcodedPasswordString) {
flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets);
if let Some(diagnostic) =
flake8_bandit::rules::assign_hardcoded_password_string(value, targets)
{
self.diagnostics.push(diagnostic);
}
}
if self.enabled(Rule::GlobalStatement) {
for target in targets.iter() {
@@ -1655,9 +1604,6 @@ where
self.diagnostics.push(diagnostic);
}
}
if self.settings.rules.enabled(Rule::TypeParamNameMismatch) {
pylint::rules::type_param_name_mismatch(self, value, targets);
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnprefixedTypeParam,
@@ -2149,7 +2095,6 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, value,
@@ -2160,8 +2105,7 @@ where
if self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
&& self.semantic.in_annotation())
{
pyupgrade::rules::use_pep604_annotation(
self, expr, slice, operator,
@@ -2237,9 +2181,6 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.is_stub {
if self.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(self, expr);
@@ -2259,7 +2200,6 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@@ -2270,8 +2210,7 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
&& self.semantic.in_annotation())
{
pyupgrade::rules::use_pep585_annotation(
self,
@@ -2336,7 +2275,6 @@ where
&& self.settings.target_version >= PythonVersion::Py37
&& !self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
self, expr,
@@ -2347,8 +2285,7 @@ where
if self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.semantic.future_annotations()
&& self.semantic.in_annotation()
&& !self.settings.pyupgrade.keep_runtime_typing)
&& self.semantic.in_annotation())
{
pyupgrade::rules::use_pep585_annotation(self, expr, &replacement);
}
@@ -2366,9 +2303,6 @@ where
if self.enabled(Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(self, expr);
}
if self.enabled(Rule::DeprecatedMockImport) {
pyupgrade::rules::deprecated_mock_attribute(self, expr);
}
@@ -2587,7 +2521,9 @@ where
flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords);
}
if self.enabled(Rule::ExecBuiltin) {
flake8_bandit::rules::exec_used(self, func);
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
self.diagnostics.push(diagnostic);
}
}
if self.enabled(Rule::BadFilePermissions) {
flake8_bandit::rules::bad_file_permissions(self, func, args, keywords);
@@ -2610,7 +2546,8 @@ where
flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords);
}
if self.enabled(Rule::HardcodedPasswordFuncArg) {
flake8_bandit::rules::hardcoded_password_func_arg(self, keywords);
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
}
if self.enabled(Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
@@ -2743,12 +2680,17 @@ where
flake8_debugger::rules::debugger_call(self, expr, func);
}
if self.enabled(Rule::PandasUseOfInplaceArgument) {
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords);
self.diagnostics.extend(
pandas_vet::rules::inplace_argument(self, expr, func, args, keywords)
.into_iter(),
);
}
pandas_vet::rules::call(self, func);
if self.enabled(Rule::PandasUseOfPdMerge) {
pandas_vet::rules::use_of_pd_merge(self, func);
if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) {
self.diagnostics.push(diagnostic);
};
}
if self.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo(
@@ -2865,13 +2807,16 @@ where
&self.settings.flake8_gettext.functions_names,
) {
if self.enabled(Rule::FStringInGetTextFuncCall) {
flake8_gettext::rules::f_string_in_gettext_func_call(self, args);
self.diagnostics
.extend(flake8_gettext::rules::f_string_in_gettext_func_call(args));
}
if self.enabled(Rule::FormatInGetTextFuncCall) {
flake8_gettext::rules::format_in_gettext_func_call(self, args);
self.diagnostics
.extend(flake8_gettext::rules::format_in_gettext_func_call(args));
}
if self.enabled(Rule::PrintfInGetTextFuncCall) {
flake8_gettext::rules::printf_in_gettext_func_call(self, args);
self.diagnostics
.extend(flake8_gettext::rules::printf_in_gettext_func_call(args));
}
}
if self.enabled(Rule::UncapitalizedEnvironmentVariables) {
@@ -2912,7 +2857,7 @@ where
flake8_use_pathlib::rules::replaceable_by_pathlib(self, func);
}
if self.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::legacy_random(self, func);
numpy::rules::numpy_legacy_random(self, func);
}
if self.any_enabled(&[
Rule::LoggingStringFormat,
@@ -3212,10 +3157,11 @@ where
flake8_2020::rules::compare(self, left, ops, comparators);
}
if self.enabled(Rule::HardcodedPasswordString) {
flake8_bandit::rules::compare_to_hardcoded_password_string(
self,
left,
comparators,
self.diagnostics.extend(
flake8_bandit::rules::compare_to_hardcoded_password_string(
left,
comparators,
),
);
}
if self.enabled(Rule::ComparisonWithItself) {
@@ -3236,6 +3182,29 @@ where
if self.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
}
if self.is_stub {
if self.any_enabled(&[
Rule::UnrecognizedPlatformCheck,
Rule::UnrecognizedPlatformName,
]) {
flake8_pyi::rules::unrecognized_platform(
self,
expr,
left,
ops,
comparators,
);
}
if self.enabled(Rule::BadVersionInfoComparison) {
flake8_pyi::rules::bad_version_info_comparison(
self,
expr,
left,
ops,
comparators,
);
}
}
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
@@ -4172,6 +4141,88 @@ impl<'a> Checker<'a> {
// Create the `Binding`.
let binding_id = self.semantic.push_binding(range, kind, flags);
let binding = self.semantic.binding(binding_id);
// Determine whether the binding shadows any existing bindings.
if let Some((stack_index, shadowed_id)) = self
.semantic
.scopes
.ancestors(self.semantic.scope_id)
.enumerate()
.find_map(|(stack_index, scope)| {
scope.get(name).and_then(|binding_id| {
let binding = self.semantic.binding(binding_id);
if binding.is_unbound() {
None
} else {
Some((stack_index, binding_id))
}
})
})
{
let shadowed = self.semantic.binding(shadowed_id);
let in_current_scope = stack_index == 0;
if !shadowed.kind.is_builtin()
&& shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
!branch_detection::different_forks(left, right, &self.semantic.stmts)
})
})
{
let shadows_import = matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
);
if binding.kind.is_loop_var() && shadows_import {
if self.enabled(Rule::ImportShadowedByLoopVar) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
} else if in_current_scope {
if !shadowed.is_used()
&& binding.redefines(shadowed)
&& (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import)
&& !(shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(self.semantic.stmts[shadowed.source.unwrap()]),
&self.semantic,
))
{
if self.enabled(Rule::RedefinedWhileUnused) {
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: name.to_string(),
line,
},
binding.range,
);
if let Some(range) = binding.parent_range(&self.semantic) {
diagnostic.set_parent(range.start());
}
self.diagnostics.push(diagnostic);
}
}
} else if shadows_import && binding.redefines(shadowed) {
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
}
}
// If there's an existing binding in this scope, copy its references.
if let Some(shadowed_id) = self.semantic.scopes[scope_id].get(name) {
@@ -4205,21 +4256,6 @@ impl<'a> Checker<'a> {
self.semantic.bindings[binding_id].references = references;
}
} else if let Some(shadowed_id) = self
.semantic
.scopes
.ancestors(scope_id)
.skip(1)
.find_map(|scope| scope.get(name))
{
// Otherwise, if there's an existing binding in a parent scope, mark it as shadowed.
let binding = self.semantic.binding(binding_id);
let shadowed = self.semantic.binding(shadowed_id);
if binding.redefines(shadowed) {
self.semantic
.shadowed_bindings
.insert(binding_id, shadowed_id);
}
}
// Add the binding to the scope.
@@ -4238,7 +4274,7 @@ impl<'a> Checker<'a> {
{
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
let scope = self.semantic.global_scope_mut();
let scope = self.semantic.scope_mut();
scope.add(builtin, binding_id);
}
}
@@ -4441,7 +4477,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_delete(&mut self, expr: &'a Expr) {
let Expr::Name(ast::ExprName { id, .. }) = expr else {
let Expr::Name(ast::ExprName { id, .. } )= expr else {
return;
};
@@ -4642,19 +4678,17 @@ impl<'a> Checker<'a> {
fn check_deferred_scopes(&mut self) {
if !self.any_enabled(&[
Rule::UnusedImport,
Rule::GlobalVariableNotAssigned,
Rule::ImportShadowedByLoopVar,
Rule::UndefinedLocalWithImportStarUsage,
Rule::RedefinedWhileUnused,
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::UndefinedExport,
Rule::UnaliasedCollectionsAbcSetImport,
Rule::UnconventionalImportAlias,
Rule::UndefinedExport,
Rule::UndefinedLocalWithImportStarUsage,
Rule::UndefinedLocalWithImportStarUsage,
Rule::UnusedImport,
]) {
return;
}
@@ -4721,8 +4755,8 @@ impl<'a> Checker<'a> {
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for scope_id in self.deferred.scopes.iter().rev().copied() {
let scope = &self.semantic.scopes[scope_id];
for scope_id in self.deferred.scopes.iter().rev() {
let scope = &self.semantic.scopes[*scope_id];
if scope.kind.is_module() {
// F822
@@ -4781,123 +4815,21 @@ impl<'a> Checker<'a> {
continue;
}
// F402
if self.enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding isn't a loop variable, abort.
let binding = &self.semantic.bindings[shadow.binding_id()];
if !binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding isn't an import, abort.
let shadowed = &self.semantic.bindings[shadow.shadowed_id()];
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &self.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
));
}
}
}
// F811
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `shadowed_bindings` if
// the bindings are in different scopes.
if self.enabled(Rule::RedefinedWhileUnused) {
for (name, binding_id) in scope.bindings() {
for shadow in self.semantic.shadowed_bindings(scope_id, binding_id) {
// If the shadowing binding is a loop variable, abort, to avoid overlap
// with F402.
let binding = &self.semantic.bindings[shadow.binding_id()];
if binding.kind.is_loop_var() {
continue;
}
// If the shadowed binding is used, abort.
let shadowed = &self.semantic.bindings[shadow.shadowed_id()];
if let Some(shadowed_id) = self.semantic.shadowed_binding(binding_id) {
let shadowed = self.semantic.binding(shadowed_id);
if shadowed.is_used() {
continue;
}
// If the shadowing binding isn't considered a "redefinition" of the
// shadowed binding, abort.
if !binding.redefines(shadowed) {
continue;
}
if shadow.same_scope() {
// If the symbol is a dummy variable, abort, unless the shadowed
// binding is an import.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) && self.settings.dummy_variable_rgx.is_match(name)
{
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
self.semantic.stmts[shadowed.source.unwrap()],
),
&self.semantic,
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
if !matches!(
shadowed.kind,
BindingKind::Import(..)
| BindingKind::FromImport(..)
| BindingKind::SubmoduleImport(..)
| BindingKind::FutureImport
) {
continue;
}
}
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &self.semantic.stmts)
})
}) {
continue;
}
#[allow(deprecated)]
let line = self.locator.compute_line_index(shadowed.range.start());
let binding = self.semantic.binding(binding_id);
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
@@ -4919,7 +4851,7 @@ impl<'a> Checker<'a> {
} else {
self.semantic
.scopes
.ancestor_ids(scope_id)
.ancestor_ids(*scope_id)
.flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter())
.copied()
.collect()

View File

@@ -14,18 +14,6 @@ use crate::rules;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct NoqaCode(&'static str, &'static str);
impl NoqaCode {
/// Return the prefix for the [`NoqaCode`], e.g., `SIM` for `SIM101`.
pub fn prefix(&self) -> &str {
self.0
}
/// Return the suffix for the [`NoqaCode`], e.g., `101` for `SIM101`.
pub fn suffix(&self) -> &str {
self.1
}
}
impl std::fmt::Debug for NoqaCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
@@ -168,8 +156,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented),
// pylint
(Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch),
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
@@ -609,10 +595,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-pyi
(Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam),
(Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub),
(Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck),
(Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison),
(Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison),
(Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison),
(Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck),
(Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName),
@@ -759,7 +741,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// numpy
(Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias),
(Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom),
(Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction),
// ruff
(Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString),
@@ -774,8 +755,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension),
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
#[cfg(feature = "unreachable-code")]
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
@@ -805,11 +784,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
// perflint
(Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast),
(Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator),
(Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop),
(Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension),
(Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy),
// flake8-fixme
(Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme),

View File

@@ -18,6 +18,7 @@ pub(crate) struct Docstring<'a> {
pub(crate) expr: &'a Expr,
/// The content of the docstring, including the leading and trailing quotes.
pub(crate) contents: &'a str,
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].
pub(crate) body_range: TextRange,
pub(crate) indentation: &'a str,

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines};
use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};
@@ -144,13 +144,15 @@ impl<'a> SectionContexts<'a> {
let mut contexts = Vec::new();
let mut last: Option<SectionContextData> = None;
let mut previous_line = None;
let mut lines = contents.universal_newlines().peekable();
for line in contents.universal_newlines() {
if previous_line.is_none() {
// skip the first line
previous_line = Some(line.as_str());
continue;
}
// Skip the first line, which is the summary.
let mut previous_line = lines.next();
while let Some(line) = lines.next() {
if let Some(section_kind) = suspected_as_section(&line, style) {
let indent = leading_space(&line);
let section_name = leading_words(&line);
@@ -160,8 +162,7 @@ impl<'a> SectionContexts<'a> {
if is_docstring_section(
&line,
section_name_range,
previous_line.as_ref(),
lines.peek(),
previous_line.unwrap_or_default(),
) {
if let Some(mut last) = last.take() {
last.range = TextRange::new(last.range.start(), line.start());
@@ -177,7 +178,7 @@ impl<'a> SectionContexts<'a> {
}
}
previous_line = Some(line);
previous_line = Some(line.as_str());
}
if let Some(mut last) = last.take() {
@@ -387,13 +388,7 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(
line: &Line,
section_name_range: TextRange,
previous_line: Option<&Line>,
next_line: Option<&Line>,
) -> bool {
// Determine whether the current line looks like a section header, e.g., "Args:".
fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool {
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
@@ -401,29 +396,13 @@ fn is_docstring_section(
return false;
}
// Determine whether the next line is an underline, e.g., "-----".
let next_line_is_underline = next_line.map_or(false, |next_line| {
let next_line = next_line.trim();
if next_line.is_empty() {
false
} else {
let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '='));
next_line_is_underline
}
});
if next_line_is_underline {
return true;
}
// Determine whether the previous line looks like the end of a paragraph.
let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| {
let previous_line = previous_line.trim();
let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| previous_line.ends_with(char));
previous_line_ends_with_punctuation || previous_line.is_empty()
});
if !previous_line_looks_like_end_of_paragraph {
let prev_line = previous_lines.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
return false;
}

View File

@@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
for selector in selectors {
if selector
.into_iter()
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
.any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule))
{
return true;
}

View File

@@ -39,6 +39,15 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
Ok(String::from_utf8(writer)?)
}
/// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`).
pub fn is_jupyter_notebook(path: &Path) -> bool {
path.extension()
.map_or(false, |ext| ext == JUPYTER_NOTEBOOK_EXT)
// For now this is feature gated here, the long term solution depends on
// https://github.com/astral-sh/ruff/issues/3410
&& cfg!(feature = "jupyter_notebook")
}
impl Cell {
/// Return the [`SourceValue`] of the cell.
fn source(&self) -> &SourceValue {
@@ -268,12 +277,11 @@ impl Notebook {
.markers()
.iter()
.rev()
.find(|m| m.source <= *offset)
else {
// There are no markers above the current offset, so we can
// stop here.
break;
};
.find(|m| m.source <= *offset) else {
// There are no markers above the current offset, so we can
// stop here.
break;
};
last_marker = Some(marker);
marker
}
@@ -437,13 +445,15 @@ impl Notebook {
}
#[cfg(test)]
mod tests {
mod test {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::JupyterIndex;
#[cfg(feature = "jupyter_notebook")]
use crate::jupyter::is_jupyter_notebook;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
@@ -502,6 +512,16 @@ mod tests {
Ok(())
}
#[test]
#[cfg(feature = "jupyter_notebook")]
fn inclusions() {
let path = Path::new("foo/bar/baz");
assert!(!is_jupyter_notebook(path));
let path = Path::new("foo/bar/baz.ipynb");
assert!(is_jupyter_notebook(path));
}
#[test]
fn test_concat_notebook() -> Result<()> {
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;

View File

@@ -25,12 +25,14 @@ isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted
isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from typing import Any
2 | | import collections
3 | | # Newline should be added here
2 | import random
3 | import math
4 | / from typing import Any
5 | | import collections
6 | | # Newline should be added here
| |_^ I001
4 | def foo():
5 | pass
7 | def foo():
8 | pass
|
= help: Organize imports

View File

@@ -9,8 +9,6 @@ pub use ruff_python_ast::source_code::round_trip;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
mod autofix;
mod checkers;
mod codes;

View File

@@ -149,14 +149,7 @@ impl Display for DisplayGroupedMessage<'_> {
if self.show_source {
use std::fmt::Write;
let mut padded = PadAdapter::new(f);
writeln!(
padded,
"{}",
MessageCodeFrame {
message,
jupyter_index: self.jupyter_index
}
)?;
writeln!(padded, "{}", MessageCodeFrame { message })?;
}
Ok(())

View File

@@ -11,7 +11,7 @@ use ruff_text_size::{TextRange, TextSize};
use ruff_python_ast::source_code::{OneIndexed, SourceLocation};
use crate::fs::relativize_path;
use crate::jupyter::{JupyterIndex, Notebook};
use crate::jupyter::Notebook;
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
@@ -72,13 +72,13 @@ impl Emitter for TextEmitter {
)?;
let start_location = message.compute_start_location();
let jupyter_index = context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index);
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {
let diagnostic_location = if let Some(jupyter_index) = context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index)
{
write!(
writer,
"cell {cell}{sep}",
@@ -114,14 +114,7 @@ impl Emitter for TextEmitter {
)?;
if self.flags.contains(EmitterFlags::SHOW_SOURCE) {
writeln!(
writer,
"{}",
MessageCodeFrame {
message,
jupyter_index
}
)?;
writeln!(writer, "{}", MessageCodeFrame { message })?;
}
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
@@ -165,7 +158,6 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
}
impl Display for MessageCodeFrame<'_> {
@@ -190,20 +182,6 @@ impl Display for MessageCodeFrame<'_> {
let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);
// If we're working on a jupyter notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_start_cell = jupyter_index
.cell(content_start_index.get())
.unwrap_or_default();
while start_index < content_start_index {
if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
break;
}
start_index = start_index.saturating_add(1);
}
}
// Trim leading empty lines.
while start_index < content_start_index {
if !source_code.line_text(start_index).trim().is_empty() {
@@ -217,21 +195,7 @@ impl Display for MessageCodeFrame<'_> {
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
// If we're working on a jupyter notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_end_cell = jupyter_index
.cell(content_end_index.get())
.unwrap_or_default();
while end_index > content_end_index {
if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
break;
}
end_index = end_index.saturating_sub(1);
}
}
// Trim trailing empty lines.
// Trim trailing empty lines
while end_index > content_end_index {
if !source_code.line_text(end_index).trim().is_empty() {
break;
@@ -260,14 +224,7 @@ impl Display for MessageCodeFrame<'_> {
title: None,
slices: vec![Slice {
source: &source.text,
line_start: self.jupyter_index.map_or_else(
|| start_index.get(),
|jupyter_index| {
jupyter_index
.cell_row(start_index.get())
.unwrap_or_default() as usize
},
),
line_start: start_index.get(),
annotations: vec![SourceAnnotation {
label: &label,
annotation_type: AnnotationType::Error,

View File

@@ -18,10 +18,8 @@ pub trait AsRule {
impl Rule {
pub fn from_code(code: &str) -> Result<Self, FromCodeError> {
let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?;
linter
.all_rules()
.find(|rule| rule.noqa_code().suffix() == code)
.ok_or(FromCodeError::Unknown)
let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?;
Ok(prefix.into_iter().next().unwrap())
}
}

View File

@@ -3,7 +3,7 @@ use ruff_macros::CacheKey;
use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
const RULESET_SIZE: usize = 11;
const RULESET_SIZE: usize = 10;
/// A set of [`Rule`]s.
///

View File

@@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector {
}
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
.chain(Linter::McCabe.rules()),
.into_iter()
.chain(Linter::McCabe.into_iter()),
),
RuleSelector::T => RuleSelectorIter::Chain(
Linter::Flake8Debugger
.rules()
.chain(Linter::Flake8Print.rules()),
.into_iter()
.chain(Linter::Flake8Print.into_iter()),
),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()),
}
}
}
@@ -346,7 +346,7 @@ mod clap_completion {
let prefix = p.linter().common_prefix();
let code = p.short_code();
let mut rules_iter = p.rules();
let mut rules_iter = p.into_iter();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use rustpython_parser::ast::Constant;
use ruff_python_ast::prelude::Constant;
use crate::checkers::ast::Checker;

View File

@@ -9,38 +9,6 @@ use crate::registry::Rule;
use super::super::helpers::is_sys;
/// ## What it does
/// Checks for comparisons that test `sys.version` against string literals,
/// such that the comparison will evaluate to `False` on Python 3.10 or later.
///
/// ## Why is this bad?
/// Comparing `sys.version` to a string is error-prone and may cause subtle
/// bugs, as the comparison will be performed lexicographically, not
/// semantically. For example, `sys.version > "3.9"` will evaluate to `False`
/// when using Python 3.10, as `"3.10"` is lexicographically "less" than
/// `"3.9"`.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version > "3.9" # `False` on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info > (3, 9) # `True` on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr3;
@@ -51,43 +19,6 @@ impl Violation for SysVersionCmpStr3 {
}
}
/// ## What it does
/// Checks for equality comparisons against the major version returned by
/// `sys.version_info` (e.g., `sys.version_info[0] == 3`).
///
/// ## Why is this bad?
/// Using `sys.version_info[0] == 3` to verify that the major version is
/// Python 3 or greater will fail if the major version number is ever
/// incremented (e.g., to Python 4). This is likely unintended, as code
/// that uses this comparison is likely intended to be run on Python 2,
/// but would now run on Python 4 too.
///
/// Instead, use `>=` to check if the major version number is 3 or greater,
/// to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info[0] == 3:
/// ...
/// else:
/// print("Python 2") # This will be printed on Python 4.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info >= (3,):
/// ...
/// else:
/// print("Python 2") # This will not be printed on Python 4.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo0Eq3;
@@ -98,36 +29,6 @@ impl Violation for SysVersionInfo0Eq3 {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version_info[1]` against an integer.
///
/// ## Why is this bad?
/// Comparisons based on the current minor version number alone can cause
/// subtle bugs and would likely lead to unintended effects if the Python
/// major version number were ever incremented (e.g., to Python 4).
///
/// Instead, compare `sys.version_info` to a tuple, including the major and
/// minor version numbers, to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info[1] < 7:
/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info < (3, 7):
/// print("Python 3.6 or earlier.")
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfo1CmpInt;
@@ -141,36 +42,6 @@ impl Violation for SysVersionInfo1CmpInt {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version_info.minor` against an integer.
///
/// ## Why is this bad?
/// Comparisons based on the current minor version number alone can cause
/// subtle bugs and would likely lead to unintended effects if the Python
/// major version number were ever incremented (e.g., to Python 4).
///
/// Instead, compare `sys.version_info` to a tuple, including the major and
/// minor version numbers, to future-proof the code.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info.minor < 7:
/// print("Python 3.6 or earlier.") # This will be printed on Python 4.0.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// if sys.version_info < (3, 7):
/// print("Python 3.6 or earlier.")
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionInfoMinorCmpInt;
@@ -184,37 +55,6 @@ impl Violation for SysVersionInfoMinorCmpInt {
}
}
/// ## What it does
/// Checks for comparisons that test `sys.version` against string literals,
/// such that the comparison would fail if the major version number were
/// ever incremented to Python 10 or higher.
///
/// ## Why is this bad?
/// Comparing `sys.version` to a string is error-prone and may cause subtle
/// bugs, as the comparison will be performed lexicographically, not
/// semantically.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version >= "3" # `False` on Python 10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info >= (3,) # `True` on Python 10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionCmpStr10;

View File

@@ -5,35 +5,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `six.PY3`.
///
/// ## Why is this bad?
/// `six.PY3` will evaluate to `False` on Python 4 and greater. This is likely
/// unintended, and may cause code intended to run on Python 2 to run on Python 4
/// too.
///
/// Instead, use `not six.PY2` to validate that the current Python major version is
/// _not_ equal to 2, to future-proof the code.
///
/// ## Example
/// ```python
/// import six
///
/// six.PY3 # `False` on Python 4.
/// ```
///
/// Use instead:
/// ```python
/// import six
///
/// not six.PY2 # `True` on Python 4.
/// ```
///
/// ## References
/// - [PyPI: `six`](https://pypi.org/project/six/)
/// - [Six documentation: `six.PY2`](https://six.readthedocs.io/#six.PY2)
/// - [Six documentation: `six.PY3`](https://six.readthedocs.io/#six.PY3)
#[violation]
pub struct SixPY3;

View File

@@ -8,36 +8,6 @@ use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::flake8_2020::helpers::is_sys;
/// ## What it does
/// Checks for uses of `sys.version[:3]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[:3]` will truncate the version number (e.g., `"3.10"` would
/// become `"3.1"`). This is likely unintended, and can lead to subtle bugs if
/// the version string is used to test against a specific Python version.
///
/// Instead, use `sys.version_info` to access the current major and minor
/// version numbers as a tuple, which can be compared to other tuples
/// without issue.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[:3] # Evaluates to "3.1" on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// sys.version_info[:2] # Evaluates to (3, 10) on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice3;
@@ -48,36 +18,6 @@ impl Violation for SysVersionSlice3 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[2]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[2]` will select the first digit of the minor number only
/// (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended, and
/// can lead to subtle bugs if the version is used to test against a minor
/// version number.
///
/// Instead, use `sys.version_info.minor` to access the current minor version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[2] # Evaluates to "1" on Python 3.10.
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.minor}" # Evaluates to "10" on Python 3.10.
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion2;
@@ -88,36 +28,6 @@ impl Violation for SysVersion2 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[0]`.
///
/// ## Why is this bad?
/// If the current major or minor version consists of multiple digits,
/// `sys.version[0]` will select the first digit of the major version number
/// only (e.g., `"3.10"` would evaluate to `"1"`). This is likely unintended,
/// and can lead to subtle bugs if the version string is used to test against a
/// major version number.
///
/// Instead, use `sys.version_info.major` to access the current major version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[0] # If using Python 10, this evaluates to "1".
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10".
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersion0;
@@ -128,36 +38,6 @@ impl Violation for SysVersion0 {
}
}
/// ## What it does
/// Checks for uses of `sys.version[:1]`.
///
/// ## Why is this bad?
/// If the major version number consists of more than one digit, this will
/// select the first digit of the major version number only (e.g., `"10.0"`
/// would evaluate to `"1"`). This is likely unintended, and can lead to subtle
/// bugs in future versions of Python if the version string is used to test
/// against a specific major version number.
///
/// Instead, use `sys.version_info.major` to access the current major version
/// number.
///
/// ## Example
/// ```python
/// import sys
///
/// sys.version[:1] # If using Python 10, this evaluates to "1".
/// ```
///
/// Use instead:
/// ```python
/// import sys
///
/// f"{sys.version_info.major}" # If using Python 10, this evaluates to "10".
/// ```
///
/// ## References
/// - [Python documentation: `sys.version`](https://docs.python.org/3/library/sys.html#sys.version)
/// - [Python documentation: `sys.version_info`](https://docs.python.org/3/library/sys.html#sys.version_info)
#[violation]
pub struct SysVersionSlice1;

View File

@@ -457,7 +457,11 @@ pub(crate) fn definition(
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member { kind, stmt, .. }) = definition else {
let Definition::Member(Member {
kind,
stmt,
..
}) = definition else {
return vec![];
};

View File

@@ -39,7 +39,6 @@ mod tests {
#[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))]
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]

View File

@@ -1,10 +1,8 @@
use rustpython_parser::ast::{Expr, Ranged};
use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[violation]
pub struct ExecBuiltin;
@@ -16,16 +14,12 @@ impl Violation for ExecBuiltin {
}
/// S102
pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) {
if checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["" | "builtin", "exec"])
})
{
checker
.diagnostics
.push(Diagnostic::new(ExecBuiltin, func.range()));
pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> {
let Expr::Name(ast::ExprName { id, .. }) = func else {
return None;
};
if id != "exec" {
return None;
}
Some(Diagnostic::new(ExecBuiltin, expr.range()))
}

View File

@@ -1,6 +1,5 @@
use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -37,7 +36,9 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option<Diagnostic> {
}
/// S107
pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) {
pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for ArgWithDefault {
def,
default,
@@ -52,7 +53,9 @@ pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Argu
continue;
};
if let Some(diagnostic) = check_password_kwarg(def, default) {
checker.diagnostics.push(diagnostic);
diagnostics.push(diagnostic);
}
}
diagnostics
}

View File

@@ -1,6 +1,5 @@
use rustpython_parser::ast::{Keyword, Ranged};
use crate::checkers::ast::Checker;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -23,10 +22,10 @@ impl Violation for HardcodedPasswordFuncArg {
}
/// S106
pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) {
checker
.diagnostics
.extend(keywords.iter().filter_map(|keyword| {
pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec<Diagnostic> {
keywords
.iter()
.filter_map(|keyword| {
string_literal(&keyword.value).filter(|string| !string.is_empty())?;
let arg = keyword.arg.as_ref()?;
if !matches_password_name(arg) {
@@ -38,5 +37,6 @@ pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Key
},
keyword.range(),
))
}));
})
.collect()
}

View File

@@ -3,8 +3,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use super::super::helpers::{matches_password_name, string_literal};
#[violation]
@@ -49,13 +47,12 @@ fn password_target(target: &Expr) -> Option<&str> {
/// S105
pub(crate) fn compare_to_hardcoded_password_string(
checker: &mut Checker,
left: &Expr,
comparators: &[Expr],
) {
checker
.diagnostics
.extend(comparators.iter().filter_map(|comp| {
) -> Vec<Diagnostic> {
comparators
.iter()
.filter_map(|comp| {
string_literal(comp).filter(|string| !string.is_empty())?;
let Some(name) = password_target(left) else {
return None;
@@ -66,29 +63,29 @@ pub(crate) fn compare_to_hardcoded_password_string(
},
comp.range(),
))
}));
})
.collect()
}
/// S105
pub(crate) fn assign_hardcoded_password_string(
checker: &mut Checker,
value: &Expr,
targets: &[Expr],
) {
) -> Option<Diagnostic> {
if string_literal(value)
.filter(|string| !string.is_empty())
.is_some()
{
for target in targets {
if let Some(name) = password_target(target) {
checker.diagnostics.push(Diagnostic::new(
return Some(Diagnostic::new(
HardcodedPasswordString {
name: name.to_string(),
},
value.range(),
));
return;
}
}
}
None
}

View File

@@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio
return None;
};
// Only evaluate the full BinOp, not the nested components.
let Expr::BinOp(_) = parent else {
let Expr::BinOp(_ )= parent else {
if any_over_expr(expr, &has_string_literal) {
return Some(checker.generator().expr(expr));
}

View File

@@ -21,6 +21,21 @@ impl Violation for RequestWithNoCertValidation {
}
}
const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
const HTTPX_METHODS: [&str; 11] = [
"get",
"options",
"head",
"post",
"put",
"patch",
"delete",
"request",
"stream",
"Client",
"AsyncClient",
];
/// S501
pub(crate) fn request_with_no_cert_validation(
checker: &mut Checker,
@@ -31,13 +46,16 @@ pub(crate) fn request_with_no_cert_validation(
if let Some(target) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| match call_path.as_slice() {
["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => {
Some("requests")
.and_then(|call_path| {
if call_path.len() == 2 {
if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) {
return Some("requests");
}
if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) {
return Some("httpx");
}
}
["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request"
| "stream" | "Client" | "AsyncClient"] => Some("httpx"),
_ => None,
None
})
{
let call_args = SimpleCallArgs::new(args, keywords);

View File

@@ -1,28 +1,31 @@
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs};
use ruff_python_ast::helpers::SimpleCallArgs;
use crate::checkers::ast::Checker;
#[violation]
pub struct RequestWithoutTimeout {
implicit: bool,
pub timeout: Option<String>,
}
impl Violation for RequestWithoutTimeout {
#[derive_message_formats]
fn message(&self) -> String {
let RequestWithoutTimeout { implicit } = self;
if *implicit {
format!("Probable use of requests call without timeout")
} else {
format!("Probable use of requests call with timeout set to `None`")
let RequestWithoutTimeout { timeout } = self;
match timeout {
Some(value) => {
format!("Probable use of requests call with timeout set to `{value}`")
}
None => format!("Probable use of requests call without timeout"),
}
}
}
const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"];
/// S113
pub(crate) fn request_without_timeout(
checker: &mut Checker,
@@ -34,26 +37,30 @@ pub(crate) fn request_without_timeout(
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(
call_path.as_slice(),
[
"requests",
"get" | "options" | "head" | "post" | "put" | "patch" | "delete"
]
)
HTTP_VERBS
.iter()
.any(|func_name| call_path.as_slice() == ["requests", func_name])
})
{
let call_args = SimpleCallArgs::new(args, keywords);
if let Some(timeout) = call_args.keyword_argument("timeout") {
if is_const_none(timeout) {
if let Some(timeout_arg) = call_args.keyword_argument("timeout") {
if let Some(timeout) = match timeout_arg {
Expr::Constant(ast::ExprConstant {
value: value @ Constant::None,
..
}) => Some(checker.generator().constant(value)),
_ => None,
} {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },
timeout.range(),
RequestWithoutTimeout {
timeout: Some(timeout),
},
timeout_arg.range(),
));
}
} else {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: true },
RequestWithoutTimeout { timeout: None },
func.range(),
));
}

View File

@@ -219,7 +219,7 @@ impl Violation for SuspiciousFTPLibUsage {
}
}
/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323
/// S001
pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return;
@@ -246,7 +246,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) {
// Mktemp
["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()),
// Eval
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
["eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen

View File

@@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected
1 | def fn():
2 | # Error
3 | exec('x = 2')
| ^^^^ S102
| ^^^^^^^^^^^^^ S102
4 |
5 | exec('y = 3')
|
@@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected
3 | exec('x = 2')
4 |
5 | exec('y = 3')
| ^^^^ S102
| ^^^^^^^^^^^^^ S102
|

View File

@@ -1,20 +0,0 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S307.py:3:7: S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
1 | import os
2 |
3 | print(eval("1+1")) # S307
| ^^^^^^^^^^^ S307
4 | print(eval("os.getcwd()")) # S307
|
S307.py:4:7: S307 Use of possibly insecure function; consider using `ast.literal_eval`
|
3 | print(eval("1+1")) # S307
4 | print(eval("os.getcwd()")) # S307
| ^^^^^^^^^^^^^^^^^^^ S307
|

View File

@@ -4,57 +4,50 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
matches!(
name,
"append"
| "assertEqual"
| "assertEquals"
| "assertNotEqual"
| "assertNotEquals"
| "bool"
| "bytes"
| "count"
| "failIfEqual"
| "failUnlessEqual"
| "float"
| "fromkeys"
| "get"
| "getattr"
| "getboolean"
| "getfloat"
| "getint"
| "index"
| "insert"
| "int"
| "param"
| "pop"
| "remove"
| "set_blocking"
| "set_enabled"
| "setattr"
| "__setattr__"
| "setdefault"
| "str"
)
}
pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[
"append",
"assertEqual",
"assertEquals",
"assertNotEqual",
"assertNotEquals",
"bool",
"bytes",
"count",
"failIfEqual",
"failUnlessEqual",
"float",
"fromkeys",
"get",
"getattr",
"getboolean",
"getfloat",
"getint",
"index",
"insert",
"int",
"param",
"pop",
"remove",
"set_blocking",
"set_enabled",
"setattr",
"__setattr__",
"setdefault",
"str",
];
/// Returns `true` if a function definition is allowed to use a boolean trap.
pub(super) fn is_allowed_func_def(name: &str) -> bool {
matches!(name, "__setitem__")
}
pub(super) const FUNC_DEF_NAME_ALLOWLIST: &[&str] = &["__setitem__"];
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func {
return is_allowed_func_call(attr);
return FUNC_CALL_NAME_ALLOWLIST.contains(&attr.as_ref());
}
if let Expr::Name(ast::ExprName { id, .. }) = func {
return is_allowed_func_call(id);
return FUNC_CALL_NAME_ALLOWLIST.contains(&id.as_ref());
}
false

View File

@@ -1,11 +1,14 @@
use rustpython_parser::ast::{ArgWithDefault, Arguments, Decorator};
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def};
use crate::rules::flake8_boolean_trap::helpers::add_if_boolean;
use super::super::helpers::FUNC_DEF_NAME_ALLOWLIST;
/// ## What it does
/// Checks for the use of booleans as default values in function definitions.
@@ -61,7 +64,7 @@ pub(crate) fn check_boolean_default_value_in_function_definition(
decorator_list: &[Decorator],
arguments: &Arguments,
) {
if is_allowed_func_def(name) {
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
return;
}

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
use crate::rules::flake8_boolean_trap::helpers::FUNC_DEF_NAME_ALLOWLIST;
/// ## What it does
/// Checks for boolean positional arguments in function definitions.
@@ -82,7 +82,7 @@ pub(crate) fn check_positional_boolean_in_def(
decorator_list: &[Decorator],
arguments: &Arguments,
) {
if is_allowed_func_def(name) {
if FUNC_DEF_NAME_ALLOWLIST.contains(&name) {
return;
}

View File

@@ -161,19 +161,19 @@ pub(crate) fn abstract_base_class(
continue;
}
let (Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
else {
let (
Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})
) = stmt else {
continue;
};

View File

@@ -1,20 +1,22 @@
use std::fmt;
use rustpython_parser::ast::{self, Expr, Ranged, WithItem};
use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AssertionKind {
AssertRaises,
PytestRaises,
}
/// ## What it does
/// Checks for `assertRaises` and `pytest.raises` context managers that catch
/// `Exception` or `BaseException`.
/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`.
///
/// ## Why is this bad?
/// These forms catch every `Exception`, which can lead to tests passing even
/// if, e.g., the code under consideration raises a `SyntaxError` or
/// `IndentationError`.
/// if, e.g., the code being tested is never executed due to a typo.
///
/// Either assert for a more specific exception (builtin or custom), or use
/// `assertRaisesRegex` or `pytest.raises(..., match=<REGEX>)` respectively.
@@ -30,83 +32,51 @@ use crate::checkers::ast::Checker;
/// ```
#[violation]
pub struct AssertRaisesException {
assertion: AssertionKind,
exception: ExceptionKind,
kind: AssertionKind,
}
impl Violation for AssertRaisesException {
#[derive_message_formats]
fn message(&self) -> String {
let AssertRaisesException {
assertion,
exception,
} = self;
format!("`{assertion}({exception})` should be considered evil")
}
}
#[derive(Debug, PartialEq, Eq)]
enum AssertionKind {
AssertRaises,
PytestRaises,
}
impl fmt::Display for AssertionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
AssertionKind::AssertRaises => fmt.write_str("assertRaises"),
AssertionKind::PytestRaises => fmt.write_str("pytest.raises"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum ExceptionKind {
BaseException,
Exception,
}
impl fmt::Display for ExceptionKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
ExceptionKind::BaseException => fmt.write_str("BaseException"),
ExceptionKind::Exception => fmt.write_str("Exception"),
match self.kind {
AssertionKind::AssertRaises => {
format!("`assertRaises(Exception)` should be considered evil")
}
AssertionKind::PytestRaises => {
format!("`pytest.raises(Exception)` should be considered evil")
}
}
}
}
/// B017
pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) {
for item in items {
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
range: _,
}) = &item.context_expr
else {
return;
};
if args.len() != 1 {
return;
}
if item.optional_vars.is_some() {
return;
}
pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) {
let Some(item) = items.first() else {
return;
};
let item_context = &item.context_expr;
let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else {
return;
};
if args.len() != 1 {
return;
}
if item.optional_vars.is_some() {
return;
}
let Some(exception) = checker
.semantic()
.resolve_call_path(args.first().unwrap())
.and_then(|call_path| match call_path.as_slice() {
["", "Exception"] => Some(ExceptionKind::Exception),
["", "BaseException"] => Some(ExceptionKind::BaseException),
_ => None,
})
else {
return;
};
if !checker
.semantic()
.resolve_call_path(args.first().unwrap())
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["", "Exception"])
})
{
return;
}
let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
let kind = {
if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises")
{
AssertionKind::AssertRaises
} else if checker
@@ -122,14 +92,11 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
AssertionKind::PytestRaises
} else {
return;
};
}
};
checker.diagnostics.push(Diagnostic::new(
AssertRaisesException {
assertion,
exception,
},
item.range(),
));
}
checker.diagnostics.push(Diagnostic::new(
AssertRaisesException { kind },
stmt.range(),
));
}

View File

@@ -59,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr])
if attr != "environ" {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else {
return;
};
if id != "os" {

View File

@@ -166,11 +166,7 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand
let mut seen: FxHashSet<CallPath> = FxHashSet::default();
let mut duplicates: FxHashMap<CallPath, Vec<&Expr>> = FxHashMap::default();
for handler in handlers {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_: Some(type_),
..
}) = handler
else {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else {
continue;
};
match type_.as_ref() {

View File

@@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses {
/// This should leave any unstarred iterables alone (subsequently raising a
/// warning for B029).
fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else {
let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else {
return vec![expr];
};
let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len());

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{self, Stmt};
use rustpython_parser::ast::{self, Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -50,9 +50,9 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return;
};
if !value.is_joined_str_expr() {
let Expr::JoinedStr ( _) = value.as_ref() else {
return;
}
};
checker
.diagnostics
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));

View File

@@ -1,8 +1,9 @@
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::includes_arg_name;
use ruff_python_ast::helpers::collect_arg_names;
use ruff_python_ast::types::Node;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -57,17 +58,19 @@ impl Violation for FunctionUsesLoopVariable {
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
loaded: Vec<&'a ast::ExprName>,
stored: Vec<&'a ast::ExprName>,
// Tuple of: name, defining expression, and defining range.
loaded: Vec<(&'a str, &'a Expr)>,
// Tuple of: name, defining expression, and defining range.
stored: Vec<(&'a str, &'a Expr)>,
}
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(name) => match &name.ctx {
ExprContext::Load => self.loaded.push(name),
ExprContext::Store => self.stored.push(name),
Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx {
ExprContext::Load => self.loaded.push((id, expr)),
ExprContext::Store => self.stored.push((id, expr)),
ExprContext::Del => {}
},
_ => visitor::walk_expr(self, expr),
@@ -77,7 +80,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> {
#[derive(Default)]
struct SuspiciousVariablesVisitor<'a> {
names: Vec<&'a ast::ExprName>,
names: Vec<(&'a str, &'a Expr)>,
safe_functions: Vec<&'a Expr>,
}
@@ -92,20 +95,17 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_body(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
self.names.extend(
visitor
.loaded
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
return;
}
Stmt::Return(ast::StmtReturn {
@@ -132,9 +132,10 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
}) => {
match func.as_ref() {
Expr::Name(ast::ExprName { id, .. }) => {
if matches!(id.as_str(), "filter" | "reduce" | "map") {
let id = id.as_str();
if id == "filter" || id == "reduce" || id == "map" {
for arg in args {
if arg.is_lambda_expr() {
if matches!(arg, Expr::Lambda(_)) {
self.safe_functions.push(arg);
}
}
@@ -158,7 +159,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
for keyword in keywords {
if keyword.arg.as_ref().map_or(false, |arg| arg == "key")
&& keyword.value.is_lambda_expr()
&& matches!(keyword.value, Expr::Lambda(_))
{
self.safe_functions.push(&keyword.value);
}
@@ -174,19 +175,17 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let mut arg_names = collect_arg_names(args);
arg_names.extend(visitor.stored.iter().map(|(id, ..)| id));
// Treat any non-arguments as "suspicious".
self.names
.extend(visitor.loaded.into_iter().filter(|loaded| {
if visitor.stored.iter().any(|stored| stored.id == loaded.id) {
return false;
}
if includes_arg_name(&loaded.id, args) {
return false;
}
true
}));
self.names.extend(
visitor
.loaded
.iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
return;
}
@@ -199,7 +198,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
#[derive(Default)]
struct NamesFromAssignmentsVisitor<'a> {
names: Vec<&'a str>,
names: FxHashSet<&'a str>,
}
/// `Visitor` to collect all names used in an assignment expression.
@@ -207,7 +206,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
self.names.push(id.as_str());
self.names.insert(id.as_str());
}
Expr::Starred(ast::ExprStarred { value, .. }) => {
self.visit_expr(value);
@@ -224,7 +223,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> {
#[derive(Default)]
struct AssignedNamesVisitor<'a> {
names: Vec<&'a str>,
names: FxHashSet<&'a str>,
}
/// `Visitor` to collect all used identifiers in a statement.
@@ -258,7 +257,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
fn visit_expr(&mut self, expr: &'a Expr) {
if expr.is_lambda_expr() {
if matches!(expr, Expr::Lambda(_)) {
// Don't recurse.
return;
}
@@ -301,15 +300,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &
// If a variable was used in a function or lambda body, and assigned in the
// loop, flag it.
for name in suspicious_variables {
if reassigned_in_loop.contains(&name.id.as_str()) {
if !checker.flake8_bugbear_seen.contains(&name) {
checker.flake8_bugbear_seen.push(name);
for (name, expr) in suspicious_variables {
if reassigned_in_loop.contains(name) {
if !checker.flake8_bugbear_seen.contains(&expr) {
checker.flake8_bugbear_seen.push(expr);
checker.diagnostics.push(Diagnostic::new(
FunctionUsesLoopVariable {
name: name.id.to_string(),
name: name.to_string(),
},
name.range(),
expr.range(),
));
}
}

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