Merge branch 'main' into evanrittenhouse_5073

This commit is contained in:
Charlie Marsh 2023-08-16 21:54:49 -04:00
commit dbe62cc741
680 changed files with 67225 additions and 47844 deletions

View File

@ -42,7 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/check_ecosystem.py
- scripts/*
formatter:
- Cargo.toml
@ -56,6 +56,7 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@ -327,7 +328,7 @@ jobs:
name: "Formatter ecosystem and progress checks"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true'
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
@ -337,9 +338,6 @@ jobs:
- name: "Formatter progress"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_log.txt | sort > $GITHUB_STEP_SUMMARY
# CPython is not black formatted, so we run only the stability check
- name: "Clone CPython 3.10"
run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
- name: "Check CPython stability"
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects

View File

@ -40,7 +40,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@2.0.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@2.0.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@ -1,5 +1,15 @@
# Breaking Changes
## 0.0.283 / 0.284
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))
Previously, when a target Python version was not specified, Ruff would use a default of Python 3.10. However, it is safer to default to an _older_ Python version to avoid assuming the availability of new features. We now default to the oldest supported Python version which is currently Python 3.8.
(We still support Python 3.7 but since [it has reached EOL](https://devguide.python.org/versions/#unsupported-versions) we've decided not to make it the default here.)
Note this change was announced in 0.0.283 but not active until 0.0.284.
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@ -131,7 +131,6 @@ At time of writing, the repository includes the following crates:
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.
@ -572,7 +571,7 @@ An alternative is to convert the perf data to `flamegraph.svg` using
[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`):
```shell
flamegraph --perfdata perf.data
flamegraph --perfdata perf.data --no-inline
```
#### Mac

33
Cargo.lock generated
View File

@ -14,6 +14,18 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
@ -800,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"anyhow",
"clap",
@ -991,6 +1003,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "imara-diff"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
dependencies = [
"ahash",
"hashbrown 0.12.3",
]
[[package]]
name = "imperative"
version = "1.0.4"
@ -2042,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@ -2119,6 +2141,7 @@ dependencies = [
"ruff",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_index",
"ruff_python_parser",
"serde",
"serde_json",
@ -2141,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@ -2197,6 +2220,7 @@ dependencies = [
"anyhow",
"clap",
"ignore",
"imara-diff",
"indicatif",
"indoc",
"itertools",
@ -2330,6 +2354,7 @@ dependencies = [
"similar",
"smallvec",
"thiserror",
"unicode-width",
]
[[package]]
@ -2353,7 +2378,6 @@ dependencies = [
"is-macro",
"itertools",
"lexical-parse-float",
"num-bigint",
"num-traits",
"rand",
"unic-ucd-category",
@ -2400,6 +2424,7 @@ dependencies = [
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_source_file",
"ruff_text_size",

View File

@ -49,6 +49,7 @@ toml = { version = "0.7.2" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
wsl = { version = "0.1.0" }
# v1.0.1

View File

@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- 📏 Over [600 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
@ -140,7 +140,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.282
rev: v0.0.284
hooks:
- id: ruff
```
@ -211,8 +211,8 @@ line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10.
target-version = "py310"
# Assume Python 3.8
target-version = "py38"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 500 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.282"
version = "0.0.284"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@ -62,8 +62,6 @@ quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { version = "1.0.16" }
serde = { workspace = true }
@ -77,7 +75,7 @@ strum_macros = { workspace = true }
thiserror = { version = "1.0.43" }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { version = "0.1.10" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
wsl = { version = "0.1.0" }

View File

@ -14,3 +14,19 @@ with open("/dev/shm/unit/test", "w") as f:
# not ok by config
with open("/foo/bar", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory
with tempfile.NamedTemporaryFile(dir="/tmp") as f:
f.write(b"def")
with tempfile.NamedTemporaryFile(dir="/var/tmp") as f:
f.write(b"def")
with tempfile.TemporaryDirectory(dir="/dev/shm") as d:
pass
with TemporaryDirectory(dir="/tmp") as d:
pass

View File

@ -68,6 +68,20 @@ def this_is_also_wrong(value={}):
...
class Foo:
@staticmethod
def this_is_also_wrong_and_more_indented(value={}):
pass
def multiline_arg_wrong(value={
}):
...
def single_line_func_wrong(value = {}): ...
def and_this(value=set()):
...
@ -261,3 +275,32 @@ def mutable_annotations(
d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
):
pass
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""
...
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""; ...
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""; \
...
def single_line_func_wrong(value: dict[str, str] = {
# This is a comment
}):
"""Docstring"""
def single_line_func_wrong(value: dict[str, str] = {}) \
: \
"""Docstring"""

View File

@ -74,3 +74,10 @@ try:
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass
# https://github.com/astral-sh/ruff/issues/6412
try:
pass
except (ValueError, ValueError, TypeError):
pass

View File

@ -1,20 +1,19 @@
import typing
# Shouldn't affect non-union field types.
field1: str
# Should emit for duplicate field types.
field2: str | str # PYI016: Duplicate union member `str`
# Should emit for union types in arguments.
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
print(arg1)
# Should emit for unions in return types.
def func2() -> str | str: # PYI016: Duplicate union member `str`
return "my string"
# Should emit in longer unions, even if not directly adjacent.
field3: str | str | int # PYI016: Duplicate union member `str`
field4: int | int | str # PYI016: Duplicate union member `int`
@ -33,3 +32,55 @@ field10: (str | int) | str # PYI016: Duplicate union member `str`
# Should emit for nested unions.
field11: dict[int | int, str]
# Should emit for unions with more than two cases
field12: int | int | int # Error
field13: int | int | int | int # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: int | int | str | int # Error
# Should emit for duplicate literal types; also covered by PYI030
field15: typing.Literal[1] | typing.Literal[1] # Error
# Shouldn't emit if in new parent type
field16: int | dict[int, str] # OK
# Shouldn't emit if not in a union parent
field17: dict[int, int] # OK
# Should emit in cases with newlines
field18: typing.Union[
set[
int # foo
],
set[
int # bar
],
] # Error, newline and comment will not be emitted in message
# Should emit in cases with `typing.Union` instead of `|`
field19: typing.Union[int, int] # Error
# Should emit in cases with nested `typing.Union`
field20: typing.Union[int, typing.Union[int, str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field21: typing.Union[int, int | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@ -74,3 +74,13 @@ field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@ -1,7 +1,6 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
@ -9,7 +8,9 @@ z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
def func(arg: type[int] | str | type[float]) -> None:
...
# OK
x: type[int, str, float]
@ -17,4 +18,14 @@ y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...
def func(arg: type[int, float] | str) -> None:
...
# OK
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker

View File

@ -1,14 +1,12 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
# OK
@ -16,5 +14,11 @@ x: type[int, str, float]
y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...
# OK
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker

View File

@ -80,3 +80,15 @@ class Test(unittest.TestCase):
def test_assert_not_regexp_matches(self):
self.assertNotRegex("abc", r"abc") # Error
def test_fail_if(self):
self.failIf("abc") # Error
def test_fail_unless(self):
self.failUnless("abc") # Error
def test_fail_unless_equal(self):
self.failUnlessEqual(1, 2) # Error
def test_fail_if_equal(self):
self.failIfEqual(1, 2) # Error

View File

@ -0,0 +1,26 @@
import pytest
@pytest.mark.parametrize("x", [1, 1, 2])
def test_error_literal(x):
...
a = 1
b = 2
c = 3
@pytest.mark.parametrize("x", [a, a, b, b, b, c])
def test_error_expr_simple(x):
...
@pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
def test_error_expr_complex(x):
...
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...

View File

@ -0,0 +1,48 @@
import unittest
class Test(unittest.TestCase):
def test_errors(self):
with self.assertRaises(ValueError):
raise ValueError
with self.assertRaises(expected_exception=ValueError):
raise ValueError
with self.failUnlessRaises(ValueError):
raise ValueError
with self.assertRaisesRegex(ValueError, "test"):
raise ValueError("test")
with self.assertRaisesRegex(ValueError, expected_regex="test"):
raise ValueError("test")
with self.assertRaisesRegex(
expected_exception=ValueError, expected_regex="test"
):
raise ValueError("test")
with self.assertRaisesRegex(
expected_regex="test", expected_exception=ValueError
):
raise ValueError("test")
with self.assertRaisesRegexp(ValueError, "test"):
raise ValueError("test")
def test_unfixable_errors(self):
with self.assertRaises(ValueError, msg="msg"):
raise ValueError
with self.assertRaises(
# comment
ValueError
):
raise ValueError
with (
self
# comment
.assertRaises(ValueError)
):
raise ValueError

View File

@ -0,0 +1,12 @@
import unittest
import pytest
class Test(unittest.TestCase):
def test_pytest_raises(self):
with pytest.raises(ValueError):
raise ValueError
def test_errors(self):
with self.assertRaises(ValueError):
raise ValueError

View File

@ -73,3 +73,7 @@ print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._asdict())
import os
os._exit()

View File

@ -0,0 +1,31 @@
## Banned modules ##
import torch
from torch import *
from tensorflow import a, b, c
import torch as torch_wearing_a_trenchcoat
# this should count as module level
x = 1; import tensorflow
# banning a module also bans any submodules
import torch.foo.bar
from tensorflow.foo import bar
from torch.foo.bar import *
# unlike TID251, inline imports are *not* banned
def my_cool_function():
import tensorflow.foo.bar
def another_cool_function():
from torch.foo import bar
def import_alias():
from torch.foo import bar
if TYPE_CHECKING:
import torch

View File

@ -0,0 +1,12 @@
from __future__ import annotations
from datetime import date
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Birthday(DeclarativeBase):
__tablename__ = "birthday"
id: Mapped[int] = mapped_column(primary_key=True)
day: Mapped[date]

View File

@ -202,3 +202,14 @@ class C:
###
def f(x: None) -> None:
_ = cast(Any, _identity)(x=x)
###
# Unused arguments with `locals`.
###
def f(bar: str):
print(locals())
class C:
def __init__(self, x) -> None:
print(locals())

View File

@ -0,0 +1,6 @@
#!/usr/bin/env python3
# A copyright notice could go here
# A linter directive could go here
x = 1

View File

@ -21,3 +21,29 @@ while i < 10:
print("error")
i += 1
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
break
except:
print("error")
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
continue
except:
print("error")
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
if i > 0:
break
except:
print("error")

View File

@ -30,3 +30,10 @@ def foo() -> None:
if __name__ == "__main__":
import g
import h; import i
if __name__ == "__main__":
import j; \
import k

View File

@ -1,11 +1,16 @@
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
# aaaa
# aaaaa
# a
# a
# aa
# aaa
# aaaa
# a
# aa
# aaa
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
if True: # noqa: E501
[12]
[12 ]
[1,2]
[1, 2]

View File

@ -61,3 +61,30 @@ if x == types.X:
#: E721
assert type(res) is int
class Foo:
def asdf(self, value: str | None):
#: E721
if type(value) is str:
...
class Foo:
def type(self):
pass
def asdf(self, value: str | None):
#: E721
if type(value) is str:
...
class Foo:
def asdf(self, value: str | None):
def type():
pass
# Okay
if type(value) is str:
...

View File

@ -92,3 +92,10 @@ match *0, 1, *2:
case 0,:
import x
import y
# Test: access a sub-importation via an alias.
import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))

View File

@ -70,3 +70,13 @@ import requests_mock as rm
def requests_mock(requests_mock: rm.Mocker):
print(rm.ANY)
import sklearn.base
import mlflow.sklearn
def f():
import sklearn
mlflow

View File

@ -145,3 +145,9 @@ def f() -> None:
obj = Foo()
obj.do_thing()
def f():
try:
pass
except Exception as _:
pass

View File

@ -0,0 +1,46 @@
class Apples:
def _init_(self): # [bad-dunder-name]
pass
def __hello__(self): # [bad-dunder-name]
print("hello")
def __init_(self): # [bad-dunder-name]
# author likely unintentionally misspelled the correct init dunder.
pass
def _init_(self): # [bad-dunder-name]
# author likely unintentionally misspelled the correct init dunder.
pass
def ___neg__(self): # [bad-dunder-name]
# author likely accidentally added an additional `_`
pass
def __inv__(self): # [bad-dunder-name]
# author likely meant to call the invert dunder method
pass
def hello(self):
print("hello")
def __init__(self):
pass
def init(self):
# valid name even though someone could accidentally mean __init__
pass
def _protected_method(self):
print("Protected")
def __private_method(self):
print("Private")
@property
def __doc__(self):
return "Docstring"
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@ -13,6 +13,7 @@ print("foo %(foo)d bar %(bar)d" % {"foo": "1", "bar": "2"})
"%(key)d" % {"key": []}
print("%d" % ("%s" % ("nested",),))
"%d" % ((1, 2, 3),)
"%d" % (1 if x > 0 else [])
# False negatives
WORD = "abc"
@ -55,3 +56,4 @@ r'\%03o' % (ord(c),)
"%d" % (len(foo),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )
"%d" % (1 if x > 0 else 2)

View File

@ -10,3 +10,4 @@ os.getenv("AA", "GOOD" + "BAD")
os.getenv("AA", "GOOD" + 1)
os.getenv("AA", "GOOD %s" % "BAD")
os.getenv("B", Z)

View File

@ -10,6 +10,8 @@ os.getenv(key="foo", default="bar")
os.getenv(key=f"foo", default="bar")
os.getenv(key="foo" + "bar", default=1)
os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
os.getenv(1 if using_clear_path else "PATH_ORIG")
AA = "aa"
os.getenv(AA)

View File

@ -19,6 +19,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d")
logging.info(msg="Hello %s")
logging.info(msg="Hello %s %s")
import warning
warning.warning("Hello %s %s", "World!")

View File

@ -15,6 +15,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d", {"objects": 1, "modifications": 1, "errors": 1})
logging.info(msg="Hello")
logging.info(msg="Hello", something="else")
import warning
warning.warning("Hello %s", "World!", "again")

View File

@ -0,0 +1,13 @@
import subprocess
# Errors.
subprocess.run("ls")
subprocess.run("ls", shell=True)
# Non-errors.
subprocess.run("ls", check=True)
subprocess.run("ls", check=False)
subprocess.run("ls", shell=True, check=True)
subprocess.run("ls", shell=True, check=False)
foo.run("ls") # Not a subprocess.run call.
subprocess.bar("ls") # Not a subprocess.run call.

View File

@ -32,3 +32,30 @@ print(
)
'{' '0}'.format(1)
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)

View File

@ -15,3 +15,17 @@ f"{0}".format(1)
print(f"{0}".format(1))
''.format(1)
'{1} {0}'.format(*args)
"{1}_{0}".format(*args, 1)
"{1}_{0}".format(*args, 1, 2)
"{1}_{0}".format(1, **kwargs)
"{1}_{0}".format(1, foo=2)
"{1}_{0}".format(1, 2, **kwargs)
"{1}_{0}".format(1, 2, foo=3, bar=4)

View File

@ -1,28 +0,0 @@
# These SHOULD change
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{1}_{0}".format(*args)
"{1}_{0}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)

View File

@ -5,11 +5,42 @@ from typing import TypeAlias
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 with generics (todo)
# UP040 simple generic
T = typing.TypeVar["T"]
x: typing.TypeAlias = list[T]
# UP040 call style generic
T = typing.TypeVar("T")
x: typing.TypeAlias = list[T]
# UP040 bounded generic (todo)
T = typing.TypeVar("T", bound=int)
x: typing.TypeAlias = list[T]
T = typing.TypeVar("T", int, str)
x: typing.TypeAlias = list[T]
# UP040 contravariant generic (todo)
T = typing.TypeVar("T", contravariant=True)
x: typing.TypeAlias = list[T]
# UP040 covariant generic (todo)
T = typing.TypeVar("T", covariant=True)
x: typing.TypeAlias = list[T]
# UP040 in class scope
T = typing.TypeVar["T"]
class Foo:
# reference to global variable
x: typing.TypeAlias = list[T]
# reference to class variable
TCLS = typing.TypeVar["TCLS"]
y: typing.TypeAlias = list[TCLS]
# UP040 wont add generics in fix
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# OK
x: TypeAlias

View File

@ -0,0 +1,5 @@
# ruff: noqa: RUF100
import os # noqa: F401
print(os.sep)

View File

@ -52,3 +52,7 @@ def good(a: int):
def another_good(a):
if a % 2 == 0:
raise GoodArgCantBeEven(a)
def another_good():
raise NotImplementedError("This is acceptable too")

View File

@ -179,16 +179,13 @@ fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. }) => {
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;
@ -212,14 +209,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
if is_only(body, child)
|| is_only(orelse, child)

View File

@ -16,9 +16,15 @@ pub(crate) fn bindings(checker: &mut Checker) {
return;
}
for binding in checker.semantic.bindings.iter() {
for binding in &*checker.semantic.bindings {
if checker.enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception() && !binding.is_used() {
if binding.kind.is_bound_exception()
&& !binding.is_used()
&& !checker
.settings
.dummy_variable_rgx
.is_match(binding.name(checker.locator))
{
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),

View File

@ -11,21 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
if let Stmt::For(ast::StmtFor {
let Stmt::For(ast::StmtFor {
target, iter, body, ..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
} else {
unreachable!("Expected Expr::For | Expr::AsyncFor");
}) = checker.semantic.current_statement()
else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
}
}

View File

@ -1,5 +1,4 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
@ -112,7 +111,11 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// 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, &checker.semantic.stmts)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
@ -168,16 +171,25 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
let Some(statement_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(statement_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
&function.decorator_list,
&checker.semantic,
)
})
{
continue;
}
}
} else {
// Only enforce cross-scope shadowing for imports.
@ -195,7 +207,11 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// 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, &checker.semantic.stmts)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
@ -231,10 +247,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
@ -260,10 +273,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if enforce_typing_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic

View File

@ -171,7 +171,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
expr.start(),
));
if pydocstyle::helpers::should_ignore_docstring(contents) {
if pydocstyle::helpers::should_ignore_docstring(expr) {
#[allow(deprecated)]
let location = checker.locator.compute_source_location(expr.start());
warn_user!(

View File

@ -80,17 +80,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is an `Union[...]` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
let is_unchecked_union = checker
.semantic
.expr_grandparent()
.and_then(Expr::as_subscript_expr)
.map_or(true, |parent| {
!checker.semantic.match_typing_expr(&parent.value, "Union")
});
if is_unchecked_union {
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
@ -206,11 +198,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.scope().kind.is_any_function() {
if checker.semantic.current_scope().kind.is_function() {
// Ignore globals.
if !checker.semantic.scope().get(id).is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
}) {
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
@ -219,7 +216,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.scope().kind
&checker.semantic.current_scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker,
@ -230,7 +227,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
if matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
pep8_naming::rules::mixed_case_variable_in_global_scope(
checker, expr, id,
);
@ -243,7 +240,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
@ -264,7 +261,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::load_before_global_declaration(checker, id, expr);
}
}
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) => {
Expr::Attribute(attribute) => {
// Ex) typing.List[...]
if checker.any_enabled(&[
Rule::FutureRewritableTypeAnnotation,
@ -326,7 +323,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
pandas_vet::rules::attr(checker, attr, value, expr);
pandas_vet::rules::attr(checker, attribute);
}
Expr::Call(
call @ ast::ExprCall {
@ -405,7 +402,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, expr);
pyupgrade::rules::format_literals(checker, &summary, call);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
@ -421,9 +418,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker,
val.as_str(),
location,
checker, val, location,
);
}
}
@ -668,7 +663,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_comprehensions::rules::unnecessary_map(
checker,
expr,
checker.semantic.expr_parent(),
checker.semantic.current_expression_parent(),
func,
args,
);
@ -678,10 +673,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker, expr, func, args, keywords,
);
}
if checker.enabled(Rule::BooleanPositionalValueInFunctionCall) {
flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call(
checker, args, func,
);
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@ -763,9 +756,19 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::PytestUnittestRaisesAssertion) {
if let Some(diagnostic) =
flake8_pytest_style::rules::unittest_raises_assertion(checker, call)
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::SubprocessPopenPreexecFn) {
pylint::rules::subprocess_popen_preexec_fn(checker, call);
}
if checker.enabled(Rule::SubprocessRunWithoutCheck) {
pylint::rules::subprocess_run_without_check(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
@ -921,7 +924,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@ -948,7 +951,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = left.as_ref()
{
@ -1082,47 +1085,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
// Avoid duplicate checks if the parent is an `|` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
let is_unchecked_union = !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
..
}))
);
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
&& is_unchecked_union
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
}
}
Expr::UnaryOp(ast::ExprUnaryOp {
op,
operand,
range: _,
}) => {
let check_not_in = checker.enabled(Rule::NotInTest);
let check_not_is = checker.enabled(Rule::NotIsTest);
if check_not_in || check_not_is {
pycodestyle::rules::not_tests(
checker,
expr,
*op,
operand,
check_not_in,
check_not_is,
);
Expr::UnaryOp(
unary_op @ ast::ExprUnaryOp {
op,
operand,
range: _,
},
) => {
if checker.any_enabled(&[Rule::NotInTest, Rule::NotIsTest]) {
pycodestyle::rules::not_tests(checker, unary_op);
}
if checker.enabled(Rule::UnaryPrefixIncrementDecrement) {
flake8_bugbear::rules::unary_prefix_increment_decrement(
@ -1147,18 +1137,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
let check_none_comparisons = checker.enabled(Rule::NoneComparison);
let check_true_false_comparisons = checker.enabled(Rule::TrueFalseComparison);
if check_none_comparisons || check_true_false_comparisons {
pycodestyle::rules::literal_comparisons(
checker,
expr,
left,
ops,
comparators,
check_none_comparisons,
check_true_false_comparisons,
);
if checker.any_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) {
pycodestyle::rules::literal_comparisons(checker, compare);
}
if checker.enabled(Rule::IsLiteral) {
pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr);
@ -1241,13 +1221,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedTempFile) {
if let Some(diagnostic) = flake8_bandit::rules::hardcoded_tmp_directory(
expr,
value,
&checker.settings.flake8_bandit.hardcoded_tmp_directory,
) {
checker.diagnostics.push(diagnostic);
}
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());

View File

@ -6,9 +6,6 @@ use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Parameters`] syntax node.
pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, parameters);
}
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
}

View File

@ -53,7 +53,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::BreakOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::break_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
@ -63,30 +63,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ContinueOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::continue_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
}
}
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
}) => {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
parameters,
body,
type_params,
range: _,
},
) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
}
@ -113,7 +107,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pep8_naming::rules::invalid_first_argument_name_for_class_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
parameters,
@ -125,7 +119,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidFirstArgumentNameForMethod) {
if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
parameters,
@ -151,11 +145,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
*is_async,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
stmt.is_async_function_def_stmt(),
);
}
if checker.enabled(Rule::CustomTypeVarReturnType) {
@ -181,19 +175,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
parameters,
);
flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
}
if checker.enabled(Rule::DunderFunctionName) {
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
checker.semantic.scope(),
checker.semantic.current_scope(),
stmt,
name,
&checker.settings.pep8_naming.ignore_names,
@ -217,6 +206,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::CachedInstanceMethod) {
flake8_bugbear::rules::cached_instance_method(checker, decorator_list);
}
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, function_def);
}
if checker.any_enabled(&[
Rule::UnnecessaryReturnNone,
Rule::ImplicitReturnValue,
@ -308,6 +300,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.any_enabled(&[
Rule::PytestParametrizeNamesWrongType,
Rule::PytestParametrizeValuesWrongType,
Rule::PytestDuplicateParametrizeTestCases,
]) {
flake8_pytest_style::rules::parametrize(checker, decorator_list);
}
@ -317,16 +310,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
]) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::BooleanPositionalArgInFunctionDefinition) {
flake8_boolean_trap::rules::check_positional_boolean_in_def(
if checker.enabled(Rule::BooleanTypeHintPositionalArgument) {
flake8_boolean_trap::rules::boolean_type_hint_positional_argument(
checker,
name,
decorator_list,
parameters,
);
}
if checker.enabled(Rule::BooleanDefaultValueInFunctionDefinition) {
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
if checker.enabled(Rule::BooleanDefaultValuePositionalArgument) {
flake8_boolean_trap::rules::boolean_default_value_positional_argument(
checker,
name,
decorator_list,
@ -348,7 +341,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt);
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
@ -519,17 +512,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(checker, class_def);
}
if checker.enabled(Rule::BadDunderMethodName) {
pylint::rules::bad_dunder_method_name(checker, body);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {
pycodestyle::rules::multiple_imports_on_one_line(checker, stmt, names);
}
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@ -565,12 +557,29 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::BannedApi) {
flake8_tidy_imports::rules::name_or_parent_is_banned(
flake8_tidy_imports::rules::banned_api(
checker,
&alias.name,
alias,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent {
module: &alias.name,
},
),
&alias,
);
}
if checker.enabled(Rule::BannedModuleLevelImports) {
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent {
module: &alias.name,
},
),
&alias,
);
}
if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
@ -685,11 +694,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
let module = module.as_deref();
let level = level.map(|level| level.to_u32());
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@ -725,16 +730,56 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(module) =
helpers::resolve_imported_module_path(level, module, checker.module_path)
{
flake8_tidy_imports::rules::name_or_parent_is_banned(checker, &module, stmt);
flake8_tidy_imports::rules::banned_api(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
),
&stmt,
);
for alias in names {
if &alias.name == "*" {
continue;
}
flake8_tidy_imports::rules::name_is_banned(
flake8_tidy_imports::rules::banned_api(
checker,
format!("{module}.{}", alias.name),
alias,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
flake8_tidy_imports::matchers::MatchName {
module: &module,
member: &alias.name,
},
),
&alias,
);
}
}
}
if checker.enabled(Rule::BannedModuleLevelImports) {
if let Some(module) =
helpers::resolve_imported_module_path(level, module, checker.module_path)
{
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
),
&stmt,
);
for alias in names {
if &alias.name == "*" {
continue;
}
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
flake8_tidy_imports::matchers::MatchName {
module: &module,
member: &alias.name,
},
),
&alias,
);
}
}
@ -766,7 +811,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module),
@ -982,7 +1027,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::nested_if_statements(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::IfWithSameArms) {
@ -1004,7 +1049,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
tryceratops::rules::type_check_without_type_error(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::OutdatedVersionBlock) {
@ -1097,8 +1142,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@ -1108,9 +1152,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
stmt,
body,
checker.semantic.stmt_parent(),
with_,
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::RedefinedLoopName) {
@ -1134,13 +1177,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
iter,
orelse,
..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
@ -1190,14 +1226,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
if checker.enabled(Rule::JumpStatementInFinally) {
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
@ -1338,8 +1367,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
@ -1403,8 +1432,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(
checker, target, value, annotation,

View File

@ -176,13 +176,12 @@ impl<'a> Checker<'a> {
///
/// If the current expression in the context is not an f-string, returns ``None``.
pub(crate) fn f_string_quote_style(&self) -> Option<Quote> {
let model = &self.semantic;
if !model.in_f_string() {
if !self.semantic.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = model.expr()?;
let expr = self.semantic.current_expression()?;
let string_range = self.indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
@ -202,7 +201,7 @@ impl<'a> Checker<'a> {
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.stmts.node_id(stmt))
.and_then(|stmt| self.semantic.statement_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
@ -264,7 +263,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_stmt(stmt);
self.semantic.push_statement(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@ -288,7 +287,7 @@ where
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.parents())
&& !helpers::in_nested_block(self.semantic.current_statements())
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@ -372,7 +371,7 @@ where
);
} else if &alias.name == "*" {
self.semantic
.scope_mut()
.current_scope_mut()
.add_star_import(StarImport { level, module });
} else {
let mut flags = BindingFlags::EXTERNAL;
@ -421,7 +420,7 @@ where
BindingKind::Global,
BindingFlags::GLOBAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@ -444,7 +443,7 @@ where
BindingKind::Nonlocal(scope_id),
BindingFlags::NONLOCAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@ -455,22 +454,16 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
parameters,
decorator_list,
type_params,
returns,
..
}) => {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@ -531,8 +524,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function,
stmt,
ExtractionTarget::Function(function_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@ -540,8 +532,7 @@ where
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt),
_ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"),
_ => unreachable!("Expected Stmt::FunctionDef"),
});
self.deferred.functions.push(self.semantic.snapshot());
@ -575,8 +566,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class,
stmt,
ExtractionTarget::Class(class_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@ -609,14 +599,7 @@ where
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
let mut handled_exceptions = Exceptions::empty();
for type_ in extract_handled_exceptions(handlers) {
@ -657,7 +640,7 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.scope().kind.is_class() {
if self.semantic.current_scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
@ -676,7 +659,7 @@ where
}
} else {
matches!(
self.semantic.scope().kind,
self.semantic.current_scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@ -743,8 +726,7 @@ where
// Step 3: Clean-up
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
let scope_id = self.semantic.scope_id;
self.deferred.scopes.push(scope_id);
self.semantic.pop_scope(); // Function scope
@ -777,7 +759,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_stmt();
self.semantic.pop_statement();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@ -813,7 +795,7 @@ where
return;
}
self.semantic.push_expr(expr);
self.semantic.push_expression(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@ -841,7 +823,7 @@ where
}) => {
if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() {
if id == "locals" && ctx.is_load() {
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.set_uses_locals();
}
}
@ -1207,7 +1189,7 @@ where
));
}
}
Expr::JoinedStr(_) => {
Expr::FString(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@ -1231,7 +1213,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expr();
self.semantic.pop_expression();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@ -1286,7 +1268,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
for value in values {
self.visit_expr(value);
}
@ -1611,7 +1593,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.stmt();
let parent = self.semantic.current_statement();
if matches!(
parent,
@ -1626,7 +1608,7 @@ impl<'a> Checker<'a> {
return;
}
if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) {
if parent.is_for_stmt() {
self.add_binding(
id,
expr.range(),
@ -1646,7 +1628,7 @@ impl<'a> Checker<'a> {
return;
}
let scope = self.semantic.scope();
let scope = self.semantic.current_scope();
if scope.kind.is_module()
&& match parent {
@ -1698,8 +1680,8 @@ impl<'a> Checker<'a> {
if self
.semantic
.expr_ancestors()
.any(|expr| expr.is_named_expr_expr())
.current_expressions()
.any(Expr::is_named_expr_expr)
{
self.add_binding(
id,
@ -1725,7 +1707,7 @@ impl<'a> Checker<'a> {
self.semantic.resolve_del(id, expr.range());
if helpers::on_conditional_branch(&mut self.semantic.parents()) {
if helpers::on_conditional_branch(&mut self.semantic.current_statements()) {
return;
}
@ -1733,7 +1715,7 @@ impl<'a> Checker<'a> {
let binding_id =
self.semantic
.push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty());
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(id, binding_id);
}
@ -1825,19 +1807,14 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body, parameters, ..
}) => {
self.visit_parameters(parameters);
self.visit_body(body);
}
_ => {
unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef")
}
if let Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
}) = self.semantic.current_statement()
{
self.visit_parameters(parameters);
self.visit_body(body);
} else {
unreachable!("Expected Stmt::FunctionDef")
}
}
}

View File

@ -94,8 +94,15 @@ pub(crate) fn check_noqa(
}
}
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if settings.rules.enabled(Rule::UnusedNOQA)
&& analyze_directives
&& !exemption.is_some_and(|exemption| match exemption {
FileExemption::All => true,
FileExemption::Codes(codes) => codes.contains(&Rule::UnusedNOQA.noqa_code()),
})
{
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(directive) => {

View File

@ -226,8 +226,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W0711") => (RuleGroup::Unspecified, rules::pylint::rules::BinaryOpException),
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
// flake8-async
@ -309,6 +311,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-tidy-imports
(Flake8TidyImports, "251") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedApi),
(Flake8TidyImports, "252") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::RelativeImports),
(Flake8TidyImports, "253") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedModuleLevelImports),
// flake8-return
(Flake8Return, "501") => (RuleGroup::Unspecified, rules::flake8_return::rules::UnnecessaryReturnNone),
@ -565,9 +568,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "701") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
// flake8-boolean-trap
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalArgInFunctionDefinition),
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValueInFunctionDefinition),
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInFunctionCall),
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument),
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument),
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall),
// flake8-unused-arguments
(Flake8UnusedArguments, "001") => (RuleGroup::Unspecified, rules::flake8_unused_arguments::rules::UnusedFunctionArgument),
@ -682,6 +685,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8PytestStyle, "011") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesTooBroad),
(Flake8PytestStyle, "012") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesWithMultipleStatements),
(Flake8PytestStyle, "013") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestIncorrectPytestImport),
(Flake8PytestStyle, "014") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestDuplicateParametrizeTestCases),
(Flake8PytestStyle, "015") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertAlwaysFalse),
(Flake8PytestStyle, "016") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFailWithoutMessage),
(Flake8PytestStyle, "017") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertInExcept),
@ -694,6 +698,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8PytestStyle, "024") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture),
(Flake8PytestStyle, "025") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture),
(Flake8PytestStyle, "026") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters),
(Flake8PytestStyle, "027") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion),
// flake8-pie
(Flake8Pie, "790") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryPass),

View File

@ -1,7 +1,6 @@
//! Extract docstrings from an AST.
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind};
/// Extract a docstring from a function or class body.
@ -28,63 +27,48 @@ pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
match definition {
Definition::Module(module) => docstring_from(module.python_ast),
Definition::Member(member) => {
if let Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) = &member.stmt
{
docstring_from(body)
} else {
None
}
}
Definition::Member(member) => docstring_from(member.body()),
}
}
#[derive(Copy, Clone)]
pub(crate) enum ExtractionTarget {
Class,
Function,
pub(crate) enum ExtractionTarget<'a> {
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub(crate) fn extract_definition<'a>(
target: ExtractionTarget,
stmt: &'a Stmt,
target: ExtractionTarget<'a>,
parent: DefinitionId,
definitions: &Definitions<'a>,
) -> Member<'a> {
match target {
ExtractionTarget::Function => match &definitions[parent] {
ExtractionTarget::Function(function) => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Function,
stmt,
kind: MemberKind::Function(function),
},
Definition::Member(Member {
kind: MemberKind::Class | MemberKind::NestedClass,
kind: MemberKind::Class(_) | MemberKind::NestedClass(_),
..
}) => Member {
parent,
kind: MemberKind::Method,
stmt,
kind: MemberKind::Method(function),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedFunction,
stmt,
kind: MemberKind::NestedFunction(function),
},
},
ExtractionTarget::Class => match &definitions[parent] {
Definition::Module(..) => Member {
ExtractionTarget::Class(class) => match &definitions[parent] {
Definition::Module(_) => Member {
parent,
kind: MemberKind::Class,
stmt,
kind: MemberKind::Class(class),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedClass,
stmt,
kind: MemberKind::NestedClass(class),
},
},
}

View File

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use path_absolutize::Absolutize;
use crate::registry::RuleSet;
@ -61,7 +61,13 @@ pub fn normalize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root:
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
#[cfg(target_arch = "wasm32")]
let cwd = Path::new(".");
#[cfg(not(target_arch = "wasm32"))]
let cwd = path_absolutize::path_dedot::CWD.as_path();
if let Ok(path) = path.strip_prefix(cwd) {
return format!("{}", path.display());
}
format!("{}", path.display())

View File

@ -67,9 +67,13 @@ impl<'a> Insertion<'a> {
TextSize::default()
};
// Skip over commented lines.
// Skip over commented lines, with whitespace separation.
for line in UniversalNewlineIterator::with_offset(locator.after(location), location) {
if line.trim_whitespace_start().starts_with('#') {
let trimmed_line = line.trim_whitespace_start();
if trimmed_line.is_empty() {
continue;
}
if trimmed_line.starts_with('#') {
location = line.full_end();
} else {
break;

View File

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::fmt::Display;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
use std::iter;
use std::path::Path;
@ -26,7 +26,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
/// Run round-trip source code generation on a given Jupyter notebook file path.
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
let mut notebook = Notebook::read(path).map_err(|err| {
let mut notebook = Notebook::from_path(path).map_err(|err| {
anyhow::anyhow!(
"Failed to read notebook file `{}`: {:?}",
path.display(),
@ -120,18 +120,30 @@ pub struct Notebook {
impl Notebook {
/// Read the Jupyter Notebook from the given [`Path`].
///
/// See also the black implementation
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
pub fn read(path: &Path) -> Result<Self, Box<Diagnostic>> {
let mut reader = BufReader::new(File::open(path).map_err(|err| {
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?);
})?))
}
/// Read the Jupyter Notebook from its JSON string.
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(contents))
}
/// Read a Jupyter Notebook from a [`Read`] implementor.
///
/// See also the black implementation
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
fn from_reader<R>(mut reader: R) -> Result<Self, Box<Diagnostic>>
where
R: Read + Seek,
{
let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| {
let mut buf = [0; 1];
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
@ -144,7 +156,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@ -159,14 +171,19 @@ impl Notebook {
Category::Syntax | Category::Eof => {
// Maybe someone saved the python sources (those with the `# %%` separator)
// as jupyter notebook instead. Let's help them.
let contents = std::fs::read_to_string(path).map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?;
let mut contents = String::new();
reader
.rewind()
.and_then(|_| reader.read_to_string(&mut contents))
.map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?;
// Check if tokenizing was successful and the file is non-empty
if lex(&contents, Mode::Module).any(|result| result.is_err()) {
Diagnostic::new(
@ -182,7 +199,7 @@ impl Notebook {
Diagnostic::new(
SyntaxError {
message: format!(
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT} extension), \
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
which must be internally stored as JSON, \
but found a Python source file: {err}"
),
@ -484,22 +501,22 @@ mod tests {
fn test_invalid() {
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
which must be internally stored as JSON, \
but found a Python source file: \
expected value at line 1 column 1"
);
let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \
but this file isn't valid JSON: \
expected value at line 1 column 1"
);
let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \
missing field `cells` at line 1 column 2"
);
@ -571,11 +588,11 @@ print("after empty cells")
}
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
Path::new("ipy_escape_command_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);

View File

@ -1,7 +1,7 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |

View File

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
@ -58,7 +59,7 @@ impl Eq for LineWidth {}
impl PartialOrd for LineWidth {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.width.partial_cmp(&other.width)
Some(self.cmp(other))
}
}
@ -83,7 +84,7 @@ impl LineWidth {
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.into();
let tab_size: usize = self.tab_size.as_usize();
for c in chars {
match c {
'\t' => {
@ -144,22 +145,22 @@ impl PartialOrd<LineLength> for LineWidth {
/// The size of a tab.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabSize(pub u8);
pub struct TabSize(NonZeroU8);
impl TabSize {
pub(crate) fn as_usize(self) -> usize {
self.0.get() as usize
}
}
impl Default for TabSize {
fn default() -> Self {
Self(4)
Self(NonZeroU8::new(4).unwrap())
}
}
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
impl From<NonZeroU8> for TabSize {
fn from(tab_size: NonZeroU8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

View File

@ -293,12 +293,10 @@ impl Display for MessageCodeFrame<'_> {
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidth::new(TAB_SIZE);
let mut line_width = LineWidth::new(TabSize::default());
for (index, c) in source.char_indices() {
let old_width = line_width.get();

View File

@ -72,7 +72,7 @@ impl RuleSet {
/// let set_1 = RuleSet::from_rules(&[Rule::AmbiguousFunctionName, Rule::AnyType]);
/// let set_2 = RuleSet::from_rules(&[
/// Rule::BadQuotesInlineString,
/// Rule::BooleanPositionalValueInFunctionCall,
/// Rule::BooleanPositionalValueInCall,
/// ]);
///
/// let union = set_1.union(&set_2);
@ -80,7 +80,7 @@ impl RuleSet {
/// assert!(union.contains(Rule::AmbiguousFunctionName));
/// assert!(union.contains(Rule::AnyType));
/// assert!(union.contains(Rule::BadQuotesInlineString));
/// assert!(union.contains(Rule::BooleanPositionalValueInFunctionCall));
/// assert!(union.contains(Rule::BooleanPositionalValueInCall));
/// ```
#[must_use]
pub const fn union(mut self, other: &Self) -> Self {
@ -132,7 +132,7 @@ impl RuleSet {
/// ])));
///
/// assert!(!set_1.intersects(&RuleSet::from_rules(&[
/// Rule::BooleanPositionalValueInFunctionCall,
/// Rule::BooleanPositionalValueInCall,
/// Rule::BadQuotesInlineString
/// ])));
/// ```

View File

@ -80,7 +80,7 @@ pub(crate) fn variable_name_task_id(
// If the keyword argument is not a string, we can't do anything.
let task_id = match &keyword.value {
Expr::Constant(constant) => match &constant.value {
Constant::Str(value) => value,
Constant::Str(ast::StringConstant { value, .. }) => value,
_ => return None,
},
_ => return None,

View File

@ -1,25 +1,25 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation(
locator: &Locator,
stmt: &Stmt,
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
annotation: &str,
source_type: PySourceType,
locator: &Locator,
) -> Result<Edit> {
let contents = &locator.contents()[stmt.range()];
let contents = &locator.contents()[statement.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count = 0u32;
for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten()
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
{
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {

View File

@ -1,53 +1,11 @@
use ruff_python_ast::{self as ast, Expr, Parameters, Stmt};
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
pub(super) fn match_function_def(
stmt: &Stmt,
) -> (&str, &Parameters, Option<&Expr>, &[Stmt], &[ast::Decorator]) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
}) => (
name,
parameters,
returns.as_ref().map(AsRef::as_ref),
body,
decorator_list,
),
_ => panic!("Found non-FunctionDef in match_function_def"),
}
}
use ruff_python_semantic::{Definition, SemanticModel};
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option<String> {
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
None
}
let function = definition.as_function_def()?;
if visibility::is_overload(&function.decorator_list, semantic) {
Some(function.name.to_string())
} else {
None
}
@ -60,19 +18,12 @@ pub(crate) fn is_overload_impl(
overloaded_name: &str,
semantic: &SemanticModel,
) -> bool {
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
false
} else {
let (name, ..) = match_function_def(stmt);
name == overloaded_name
}
} else {
let Some(function) = definition.as_function_def() else {
return false;
};
if visibility::is_overload(&function.decorator_list, semantic) {
false
} else {
function.name.as_str() == overloaded_name
}
}

View File

@ -1,23 +1,19 @@
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Ranged, Stmt};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind};
use ruff_python_semantic::Definition;
use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
use super::super::fixes;
use super::super::helpers::match_function_def;
/// ## What it does
/// Checks that function arguments have type annotations.
///
@ -498,20 +494,23 @@ pub(crate) fn definition(
definition: &Definition,
visibility: visibility::Visibility,
) -> Vec<Diagnostic> {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member { kind, stmt, .. }) = definition else {
let Some(function) = definition.as_function_def() else {
return vec![];
};
let is_method = match kind {
MemberKind::Method => true,
MemberKind::Function | MemberKind::NestedFunction => false,
_ => return vec![],
};
let ast::StmtFunctionDef {
range: _,
is_async: _,
decorator_list,
name,
type_params: _,
parameters,
returns,
body,
} = function;
let is_method = definition.is_method();
let (name, arguments, returns, body, decorator_list) = match_function_def(stmt);
// Keep track of whether we've seen any typed arguments or return values.
let mut has_any_typed_arg = false; // Any argument has been typed?
let mut has_typed_return = false; // Return value has been typed?
@ -528,20 +527,19 @@ pub(crate) fn definition(
parameter,
default: _,
range: _,
} in arguments
} in parameters
.posonlyargs
.iter()
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method
&& !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()),
is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()),
),
)
{
// ANN401 for dynamically typed arguments
// ANN401 for dynamically typed parameters
if let Some(annotation) = &parameter.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) && !is_overridden {
@ -572,7 +570,7 @@ pub(crate) fn definition(
}
// ANN002, ANN401
if let Some(arg) = &arguments.vararg {
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@ -598,7 +596,7 @@ pub(crate) fn definition(
}
// ANN003, ANN401
if let Some(arg) = &arguments.kwarg {
if let Some(arg) = &parameters.kwarg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@ -629,18 +627,18 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) {
if is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) {
if let Some(ParameterWithDefault {
parameter,
default: _,
range: _,
}) = arguments
}) = parameters
.posonlyargs
.first()
.or_else(|| arguments.args.first())
.or_else(|| parameters.args.first())
{
if parameter.annotation.is_none() {
if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
@ -676,24 +674,22 @@ pub(crate) fn definition(
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
} else if is_method
&& visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic())
{
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
} else if is_method && visibility::is_init(name) {
@ -705,15 +701,15 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
checker.locator(),
stmt,
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
@ -727,16 +723,16 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
checker.locator(),
stmt,
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
@ -752,7 +748,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}
@ -762,7 +758,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}

View File

@ -112,21 +112,21 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, model: &SemanticModel) -> Option<u16> {
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_u16(),
Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
}) => {
let left_value = int_value(left, model)?;
let right_value = int_value(right, model)?;
let left_value = int_value(left, semantic)?;
let right_value = int_value(right, semantic)?;
match op {
Operator::BitAnd => Some(left_value & right_value),
Operator::BitOr => Some(left_value | right_value),

View File

@ -53,7 +53,7 @@ fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool {
fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
@ -62,8 +62,8 @@ fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool
..
}) => {
// Only evaluate the full BinOp, not the nested components.
if model
.expr_parent()
if semantic
.current_expression_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
if any_over_expr(expr, &has_string_literal) {
@ -80,7 +80,7 @@ fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool
attr == "format" && string_literal(value).is_some()
}
// f"select * from table where val = {val}"
Expr::JoinedStr(_) => true,
Expr::FString(_) => true,
_ => false,
}
}

View File

@ -1,8 +1,10 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the use of hardcoded temporary file or directory paths.
///
@ -49,19 +51,33 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(
expr: &Expr,
value: &str,
prefixes: &[String],
) -> Option<Diagnostic> {
if prefixes.iter().any(|prefix| value.starts_with(prefix)) {
Some(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
))
} else {
None
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, expr: &Expr, value: &str) {
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| value.starts_with(prefix))
{
return;
}
if let Some(Expr::Call(ast::ExprCall { func, .. })) =
checker.semantic().current_expression_parent()
{
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["tempfile", ..]))
{
return;
}
}
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
));
}

View File

@ -10,6 +10,31 @@ use crate::{
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
};
/// ## What it does
/// Check for method calls that initiate a subprocess with a shell.
///
/// ## Why is this bad?
/// Starting a subprocess with a shell can allow attackers to execute arbitrary
/// shell commands. Consider starting the process without a shell call and
/// sanitize the input to mitigate the risk of shell injection.
///
/// ## Example
/// ```python
/// import subprocess
///
/// subprocess.run("ls -l", shell=True)
/// ```
///
/// Use instead:
/// ```python
/// import subprocess
///
/// subprocess.run(["ls", "-l"])
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
#[violation]
pub struct SubprocessPopenWithShellEqualsTrue {
seems_safe: bool,
@ -28,6 +53,30 @@ impl Violation for SubprocessPopenWithShellEqualsTrue {
}
}
/// ## What it does
/// Check for method calls that initiate a subprocess without a shell.
///
/// ## Why is this bad?
/// Starting a subprocess without a shell can prevent attackers from executing
/// arbitrary shell commands; however, it is still error-prone. Consider
/// validating the input.
///
/// ## Known problems
/// Prone to false positives as it is difficult to determine whether the
/// passed arguments have been validated ([#4045]).
///
/// ## Example
/// ```python
/// import subprocess
///
/// cmd = input("Enter a command: ").split()
/// subprocess.run(cmd)
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
///
/// [#4045]: https://github.com/astral-sh/ruff/issues/4045
#[violation]
pub struct SubprocessWithoutShellEqualsTrue;

View File

@ -76,7 +76,7 @@ impl Violation for SuspiciousPickleUsage {
/// import marshal
///
/// with open("foo.marshal", "rb") as file:
/// foo = pickle.load(file)
/// foo = marshal.load(file)
/// ```
///
/// Use instead:

View File

@ -1,8 +1,4 @@
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
use ruff_python_ast::{self as ast, Constant, Expr};
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
@ -62,18 +58,13 @@ pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
false
}
const fn is_boolean_arg(arg: &Expr) -> bool {
/// Returns `true` if an expression is a boolean literal.
pub(super) const fn is_boolean(expr: &Expr) -> bool {
matches!(
&arg,
&expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(_),
..
})
)
}
pub(super) fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) {
if is_boolean_arg(arg) {
checker.diagnostics.push(Diagnostic::new(kind, arg.range()));
}
}

View File

@ -13,9 +13,9 @@ mod tests {
use crate::test::test_path;
use crate::{assert_messages, settings};
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInFunctionCall, Path::new("FBT.py"))]
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValuePositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInCall, Path::new("FBT.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@ -0,0 +1,126 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{is_allowed_func_def, is_boolean};
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a boolean default value.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller and to future
/// readers of the code.
///
/// The use of a boolean will also limit the function to only two possible
/// behaviors, which makes the function difficult to extend in the future.
///
/// Instead, consider refactoring into separate implementations for the
/// `True` and `False` cases, using an `Enum`, or making the argument a
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number, up=True):
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, True) # What does `True` mean?
/// round_number(1.5, False) # What does `False` mean?
/// ```
///
/// Instead, refactor into separate implementations:
/// ```python
/// from math import ceil, floor
///
///
/// def round_up(number):
/// return ceil(number)
///
///
/// def round_down(number):
/// return floor(number)
///
///
/// round_up(1.5)
/// round_down(1.5)
/// ```
///
/// Or, refactor to use an `Enum`:
/// ```python
/// from enum import Enum
///
///
/// class RoundingMethod(Enum):
/// UP = 1
/// DOWN = 2
///
///
/// def round_number(value, method):
/// ...
/// ```
///
/// Or, make the argument a keyword-only argument:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number, *, up=True):
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanDefaultValuePositionalArgument;
impl Violation for BooleanDefaultValuePositionalArgument {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean default positional argument in function definition")
}
}
pub(crate) fn boolean_default_value_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default,
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
if default.as_ref().is_some_and(|default| is_boolean(default)) {
checker.diagnostics.push(Diagnostic::new(
BooleanDefaultValuePositionalArgument,
parameter.name.range(),
));
}
}
}

View File

@ -1,11 +1,9 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_trap};
use crate::rules::flake8_boolean_trap::helpers::{allow_boolean_trap, is_boolean};
/// ## What it does
/// Checks for boolean positional arguments in function calls.
@ -17,44 +15,42 @@ use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_t
///
/// ## Example
/// ```python
/// def foo(flag: bool) -> None:
/// def func(flag: bool) -> None:
/// ...
///
///
/// foo(True)
/// func(True)
/// ```
///
/// Use instead:
/// ```python
/// def foo(flag: bool) -> None:
/// def func(flag: bool) -> None:
/// ...
///
///
/// foo(flag=True)
/// func(flag=True)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalValueInFunctionCall;
pub struct BooleanPositionalValueInCall;
impl Violation for BooleanPositionalValueInFunctionCall {
impl Violation for BooleanPositionalValueInCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional value in function call")
}
}
pub(crate) fn check_boolean_positional_value_in_function_call(
checker: &mut Checker,
args: &[Expr],
func: &Expr,
) {
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
if allow_boolean_trap(func) {
return;
}
for arg in args {
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
for arg in args.iter().filter(|arg| is_boolean(arg)) {
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));
}
}

View File

@ -11,16 +11,22 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## What it does
/// Checks for boolean positional arguments in function definitions.
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a `bool` type hint.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller, and to future
/// meaning of the boolean value is not clear to the caller and to future
/// readers of the code.
///
/// The use of a boolean will also limit the function to only two possible
/// behaviors, which makes the function difficult to extend in the future.
///
/// Instead, consider refactoring into separate implementations for the
/// `True` and `False` cases, using an `Enum`, or making the argument a
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// ## Example
/// ```python
/// from math import ceil, floor
@ -65,20 +71,33 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ...
/// ```
///
/// Or, make the argument a keyword-only argument:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalArgInFunctionDefinition;
pub struct BooleanTypeHintPositionalArgument;
impl Violation for BooleanPositionalArgInFunctionDefinition {
impl Violation for BooleanTypeHintPositionalArgument {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional arg in function definition")
format!("Boolean-typed positional argument in function definition")
}
}
pub(crate) fn check_positional_boolean_in_def(
pub(crate) fn boolean_type_hint_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
@ -101,28 +120,25 @@ pub(crate) fn check_positional_boolean_in_def(
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
if parameter.annotation.is_none() {
continue;
}
let Some(expr) = &parameter.annotation else {
let Some(annotation) = parameter.annotation.as_ref() else {
continue;
};
// check for both bool (python class) and 'bool' (string annotation)
let hint = match expr.as_ref() {
let hint = match annotation.as_ref() {
Expr::Name(name) => &name.id == "bool",
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) => value == "bool",
_ => false,
};
if !hint {
if !hint || !checker.semantic().is_builtin("bool") {
continue;
}
checker.diagnostics.push(Diagnostic::new(
BooleanPositionalArgInFunctionDefinition,
parameter.range(),
BooleanTypeHintPositionalArgument,
parameter.name.range(),
));
}
}

View File

@ -1,90 +0,0 @@
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
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};
/// ## What it does
/// Checks for the use of booleans as default values in function definitions.
///
/// ## Why is this bad?
/// Calling a function with boolean default means that the keyword argument
/// argument can be omitted, which makes the function call ambiguous.
///
/// Instead, define the relevant argument as keyword-only.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool = True) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5)
/// round_number(1.5, up=False)
/// ```
///
/// Use instead:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanDefaultValueInFunctionDefinition;
impl Violation for BooleanDefaultValueInFunctionDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean default value in function definition")
}
}
pub(crate) fn check_boolean_default_value_in_function_definition(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter: _,
default,
range: _,
} in parameters.args.iter().chain(&parameters.posonlyargs)
{
let Some(default) = default else {
continue;
};
add_if_boolean(
checker,
default,
BooleanDefaultValueInFunctionDefinition.into(),
);
}
}

View File

@ -1,7 +1,7 @@
pub(crate) use check_boolean_default_value_in_function_definition::*;
pub(crate) use check_boolean_positional_value_in_function_call::*;
pub(crate) use check_positional_boolean_in_def::*;
pub(crate) use boolean_default_value_positional_argument::*;
pub(crate) use boolean_positional_value_in_call::*;
pub(crate) use boolean_type_hint_positional_argument::*;
mod check_boolean_default_value_in_function_definition;
mod check_boolean_positional_value_in_function_call;
mod check_positional_boolean_in_def;
mod boolean_default_value_positional_argument;
mod boolean_positional_value_in_call;
mod boolean_type_hint_positional_argument;

View File

@ -1,91 +1,91 @@
---
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:4:5: FBT001 Boolean positional arg in function definition
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
2 | posonly_nohint,
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
| ^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^ FBT001
5 | posonly_boolstrhint: "bool",
6 | /,
|
FBT.py:5:5: FBT001 Boolean positional arg in function definition
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
5 | posonly_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^ FBT001
6 | /,
7 | offset,
|
FBT.py:10:5: FBT001 Boolean positional arg in function definition
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
8 | posorkw_nonvalued_nohint,
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
|
FBT.py:11:5: FBT001 Boolean positional arg in function definition
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
11 | posorkw_nonvalued_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
|
FBT.py:14:5: FBT001 Boolean positional arg in function definition
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
16 | posorkw_nonboolvalued_nohint=1,
|
FBT.py:15:5: FBT001 Boolean positional arg in function definition
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
FBT.py:18:5: FBT001 Boolean positional arg in function definition
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
20 | *,
|
FBT.py:19:5: FBT001 Boolean positional arg in function definition
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
20 | *,
21 | kwonly_nonvalued_nohint,
|
FBT.py:86:19: FBT001 Boolean positional arg in function definition
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
|
85 | # FBT001: Boolean positional arg in function definition
86 | def foo(self, value: bool) -> None:
| ^^^^^^^^^^^ FBT001
| ^^^^^ FBT001
87 | pass
|

View File

@ -1,42 +1,42 @@
---
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:12:31: FBT002 Boolean default value in function definition
FBT.py:12:5: FBT002 Boolean default positional argument in function definition
|
10 | posorkw_nonvalued_boolhint: bool,
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
|
FBT.py:13:43: FBT002 Boolean default value in function definition
FBT.py:13:5: FBT002 Boolean default positional argument in function definition
|
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
FBT.py:14:41: FBT002 Boolean default value in function definition
FBT.py:14:5: FBT002 Boolean default positional argument in function definition
|
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
16 | posorkw_nonboolvalued_nohint=1,
|
FBT.py:15:46: FBT002 Boolean default value in function definition
FBT.py:15:5: FBT002 Boolean default positional argument in function definition
|
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|

View File

@ -49,7 +49,6 @@ mod tests {
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@ -60,6 +59,17 @@ mod tests {
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";
let diagnostics = test_path(
Path::new("flake8_bugbear").join(snapshot).as_path(),
&Settings::for_rule(Rule::ZipWithoutExplicitStrict),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
@ -72,7 +82,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![Rule::FunctionCallInDefaultArgument])
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
},
)?;
assert_messages!(snapshot, diagnostics);

View File

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

View File

@ -77,7 +77,7 @@ fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
/// B019
pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) {
if !checker.semantic().scope().kind.is_class() {
if !checker.semantic().current_scope().kind.is_class() {
return;
}
for decorator in decorator_list {

View File

@ -150,7 +150,9 @@ fn duplicate_handler_exceptions<'a>(
if unique_elts.len() == 1 {
checker.generator().expr(unique_elts[0])
} else {
checker.generator().expr(&type_pattern(unique_elts))
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
)));

View File

@ -50,7 +50,7 @@ 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() {
if !value.is_f_string_expr() {
return;
}
checker

View File

@ -67,9 +67,9 @@ struct ArgumentDefaultVisitor<'a> {
}
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
semantic: model,
semantic,
extend_immutable_calls,
diagnostics: Vec::new(),
}

View File

@ -86,9 +86,6 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
parameters, body, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
parameters, body, ..
}) => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
@ -236,7 +233,7 @@ struct AssignedNamesVisitor<'a> {
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if matches!(stmt, Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_)) {
if stmt.is_function_def_stmt() {
// Don't recurse.
return;
}
@ -251,8 +248,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign { target, .. })
| Stmt::For(ast::StmtFor { target, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { target, .. }) => {
| Stmt::For(ast::StmtFor { target, .. }) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
self.names.extend(visitor.names);

View File

@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant(
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = arg
else {

View File

@ -69,16 +69,12 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
));
}
match stmt {
Stmt::While(ast::StmtWhile { body, .. })
| Stmt::For(ast::StmtFor { body, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, .. }) => {
Stmt::While(ast::StmtWhile { body, .. }) | Stmt::For(ast::StmtFor { body, .. }) => {
walk_stmt(checker, body, Stmt::is_return_stmt);
}
Stmt::If(ast::StmtIf { body, .. })
| Stmt::Try(ast::StmtTry { body, .. })
| Stmt::TryStar(ast::StmtTryStar { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. }) => {
walk_stmt(checker, body, f);
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {

View File

@ -1,10 +1,15 @@
use ruff_python_ast::{ParameterWithDefault, Parameters, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault, Ranged};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_index::Indexer;
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of mutable objects as function argument defaults.
@ -50,24 +55,30 @@ use crate::checkers::ast::Checker;
pub struct MutableArgumentDefault;
impl Violation for MutableArgumentDefault {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not use mutable data structures for argument defaults")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `None`; initialize within function"))
}
}
/// B006
pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Parameters) {
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
// Scan in reverse order to right-align zip().
for ParameterWithDefault {
parameter,
default,
range: _,
} in parameters
} in function_def
.parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.chain(&function_def.parameters.args)
.chain(&function_def.parameters.kwonlyargs)
{
let Some(default) = default else {
continue;
@ -79,9 +90,84 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Param
.as_ref()
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(MutableArgumentDefault, default.range()));
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
// If the function body is on the same line as the function def, do not fix
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = move_initialization(
function_def,
parameter,
default,
checker.locator(),
checker.stylist(),
checker.indexer(),
checker.generator(),
) {
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}
}
/// Generate a [`Fix`] to move a mutable argument default initialization
/// into the function body.
fn move_initialization(
function_def: &ast::StmtFunctionDef,
parameter: &Parameter,
default: &Expr,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
generator: Generator,
) -> Option<Fix> {
let mut body = function_def.body.iter();
let statement = body.next()?;
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
// Determine the indentation depth of the function body.
let indentation = indentation_at_offset(statement.start(), locator)?;
// Set the default argument value to `None`.
let default_edit = Edit::range_replacement("None".to_string(), default.range());
// Add an `if`, to set the argument to its original value if still `None`.
let mut content = String::new();
content.push_str(&format!("if {} is None:", parameter.name.as_str()));
content.push_str(stylist.line_ending().as_str());
content.push_str(stylist.indentation());
content.push_str(&format!(
"{} = {}",
parameter.name.as_str(),
generator.expr(default)
));
content.push_str(stylist.line_ending().as_str());
// Indent the edit to match the body indentation.
let content = textwrap::indent(&content, indentation).to_string();
let initialization_edit = if is_docstring_stmt(statement) {
// If the first statement in the function is a docstring, insert _after_ it.
if let Some(statement) = body.next() {
// If there's a second statement, insert _before_ it, but ensure this isn't a
// multi-statement line.
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))
} else {
// If the docstring is the only statement, insert _before_ it.
Edit::insertion(content, locator.full_line_end(statement.end()))
}
} else {
// Otherwise, insert before the first statement.
let at = locator.line_start(statement.start());
Edit::insertion(content, at)
};
Some(Fix::manual_edits(default_edit, [initialization_edit]))
}

View File

@ -88,7 +88,7 @@ pub(crate) fn setattr_with_constant(
if !is_identifier(name) {
return;
}
if is_mangled_private(name.as_str()) {
if is_mangled_private(name) {
return;
}
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
@ -97,7 +97,7 @@ pub(crate) fn setattr_with_constant(
if let Stmt::Expr(ast::StmtExpr {
value: child,
range: _,
}) = checker.semantic().stmt()
}) = checker.semantic().current_statement()
{
if expr == child.as_ref() {
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());

View File

@ -68,13 +68,13 @@ pub(crate) fn unreliable_callable_check(
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = attr
else {
return;
};
if string != "__call__" {
if value != "__call__" {
return;
}

View File

@ -159,7 +159,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
if certainty.into() {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
let scope = checker.semantic().current_scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))

View File

@ -54,7 +54,7 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::JoinedStr(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Ellipsis,
..

View File

@ -1,123 +1,479 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument defaults
|
63 | def this_is_wrong(value=[1, 2, 3]):
| ^^^^^^^^^ B006
64 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
Possible fix
60 60 | # Flag mutable literals/comprehensions
61 61 |
62 62 |
63 |-def this_is_wrong(value=[1, 2, 3]):
63 |+def this_is_wrong(value=None):
64 |+ if value is None:
65 |+ value = [1, 2, 3]
64 66 | ...
65 67 |
66 68 |
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
|
67 | def this_is_also_wrong(value={}):
| ^^ B006
68 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:71:20: B006 Do not use mutable data structures for argument defaults
|
71 | def and_this(value=set()):
| ^^^^^ B006
72 | ...
|
Possible fix
64 64 | ...
65 65 |
66 66 |
67 |-def this_is_also_wrong(value={}):
67 |+def this_is_also_wrong(value=None):
68 |+ if value is None:
69 |+ value = {}
68 70 | ...
69 71 |
70 72 |
B006_B008.py:75:20: B006 Do not use mutable data structures for argument defaults
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
|
75 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
76 | ...
71 | class Foo:
72 | @staticmethod
73 | def this_is_also_wrong_and_more_indented(value={}):
| ^^ B006
74 | pass
|
= help: Replace with `None`; initialize within function
B006_B008.py:79:32: B006 Do not use mutable data structures for argument defaults
Possible fix
70 70 |
71 71 | class Foo:
72 72 | @staticmethod
73 |- def this_is_also_wrong_and_more_indented(value={}):
73 |+ def this_is_also_wrong_and_more_indented(value=None):
74 |+ if value is None:
75 |+ value = {}
74 76 | pass
75 77 |
76 78 |
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
|
77 | def multiline_arg_wrong(value={
| _______________________________^
78 | |
79 | | }):
| |_^ B006
80 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
74 74 | pass
75 75 |
76 76 |
77 |-def multiline_arg_wrong(value={
78 |-
79 |-}):
77 |+def multiline_arg_wrong(value=None):
78 |+ if value is None:
79 |+ value = {}
80 80 | ...
81 81 |
82 82 | def single_line_func_wrong(value = {}): ...
B006_B008.py:82:36: B006 Do not use mutable data structures for argument defaults
|
79 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
80 | ...
81 |
82 | def single_line_func_wrong(value = {}): ...
| ^^ B006
|
= help: Replace with `None`; initialize within function
B006_B008.py:83:26: B006 Do not use mutable data structures for argument defaults
B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument defaults
|
83 | def dont_forget_me(value=collections.deque()):
85 | def and_this(value=set()):
| ^^^^^ B006
86 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
82 82 | def single_line_func_wrong(value = {}): ...
83 83 |
84 84 |
85 |-def and_this(value=set()):
85 |+def and_this(value=None):
86 |+ if value is None:
87 |+ value = set()
86 88 | ...
87 89 |
88 90 |
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
|
89 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
90 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
86 86 | ...
87 87 |
88 88 |
89 |-def this_too(value=collections.OrderedDict()):
89 |+def this_too(value=None):
90 |+ if value is None:
91 |+ value = collections.OrderedDict()
90 92 | ...
91 93 |
92 94 |
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
|
93 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
94 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
90 90 | ...
91 91 |
92 92 |
93 |-async def async_this_too(value=collections.defaultdict()):
93 |+async def async_this_too(value=None):
94 |+ if value is None:
95 |+ value = collections.defaultdict()
94 96 | ...
95 97 |
96 98 |
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
|
97 | def dont_forget_me(value=collections.deque()):
| ^^^^^^^^^^^^^^^^^^^ B006
84 | ...
98 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:88:46: B006 Do not use mutable data structures for argument defaults
|
87 | # N.B. we're also flagging the function call in the comprehension
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
89 | pass
|
Possible fix
94 94 | ...
95 95 |
96 96 |
97 |-def dont_forget_me(value=collections.deque()):
97 |+def dont_forget_me(value=None):
98 |+ if value is None:
99 |+ value = collections.deque()
98 100 | ...
99 101 |
100 102 |
B006_B008.py:92:46: B006 Do not use mutable data structures for argument defaults
|
92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
93 | pass
|
B006_B008.py:96:45: B006 Do not use mutable data structures for argument defaults
|
96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
97 | pass
|
B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaults
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
|
100 | def kwonlyargs_mutable(*, value=[]):
101 | # N.B. we're also flagging the function call in the comprehension
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
103 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
99 99 |
100 100 |
101 101 | # N.B. we're also flagging the function call in the comprehension
102 |-def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
102 |+def list_comprehension_also_not_okay(default=None):
103 |+ if default is None:
104 |+ default = [i ** 2 for i in range(3)]
103 105 | pass
104 106 |
105 107 |
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
|
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
107 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
103 103 | pass
104 104 |
105 105 |
106 |-def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
106 |+def dict_comprehension_also_not_okay(default=None):
107 |+ if default is None:
108 |+ default = {i: i ** 2 for i in range(3)}
107 109 | pass
108 110 |
109 111 |
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
|
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
111 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
107 107 | pass
108 108 |
109 109 |
110 |-def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
110 |+def set_comprehension_also_not_okay(default=None):
111 |+ if default is None:
112 |+ default = {i ** 2 for i in range(3)}
111 113 | pass
112 114 |
113 115 |
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
|
114 | def kwonlyargs_mutable(*, value=[]):
| ^^ B006
101 | ...
115 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaults
Possible fix
111 111 | pass
112 112 |
113 113 |
114 |-def kwonlyargs_mutable(*, value=[]):
114 |+def kwonlyargs_mutable(*, value=None):
115 |+ if value is None:
116 |+ value = []
115 117 | ...
116 118 |
117 119 |
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
222 | pass
236 | pass
|
= help: Replace with `None`; initialize within function
B006_B008.py:258:27: B006 Do not use mutable data structures for argument defaults
Possible fix
232 232 |
233 233 | # B006 and B008
234 234 | # We should handle arbitrary nesting of these B008.
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
235 |+def nested_combo(a=None):
236 |+ if a is None:
237 |+ a = [float(3), dt.datetime.now()]
236 238 | pass
237 239 |
238 240 |
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
271 | def mutable_annotations(
272 | a: list[int] | None = [],
| ^^ B006
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
B006_B008.py:259:35: B006 Do not use mutable data structures for argument defaults
Possible fix
269 269 |
270 270 |
271 271 | def mutable_annotations(
272 |- a: list[int] | None = [],
272 |+ a: list[int] | None = None,
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if a is None:
278 |+ a = []
277 279 | pass
278 280 |
279 281 |
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
271 | def mutable_annotations(
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
| ^^ B006
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
B006_B008.py:260:62: B006 Do not use mutable data structures for argument defaults
Possible fix
270 270 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 |- b: Optional[Dict[int, int]] = {},
273 |+ b: Optional[Dict[int, int]] = None,
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if b is None:
278 |+ b = {}
277 279 | pass
278 280 |
279 281 |
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
262 | ):
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | ):
|
= help: Replace with `None`; initialize within function
B006_B008.py:261:80: B006 Do not use mutable data structures for argument defaults
Possible fix
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if c is None:
278 |+ c = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
262 | ):
263 | pass
276 | ):
277 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
276 276 | ):
277 |+ if d is None:
278 |+ d = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
280 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
281 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
277 277 | pass
278 278 |
279 279 |
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
280 |+def single_line_func_wrong(value: dict[str, str] = None):
281 281 | """Docstring"""
282 |+ if value is None:
283 |+ value = {}
282 284 |
283 285 |
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
285 | """Docstring"""
286 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
281 281 | """Docstring"""
282 282 |
283 283 |
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
284 |+def single_line_func_wrong(value: dict[str, str] = None):
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 | ...
287 289 |
288 290 |
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
289 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
290 | """Docstring"""; ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
294 | """Docstring"""; \
295 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
298 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
299 | | # This is a comment
300 | | }):
| |_^ B006
301 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
295 295 | ...
296 296 |
297 297 |
298 |-def single_line_func_wrong(value: dict[str, str] = {
299 |- # This is a comment
300 |-}):
298 |+def single_line_func_wrong(value: dict[str, str] = None):
301 299 | """Docstring"""
300 |+ if value is None:
301 |+ value = {}
302 302 |
303 303 |
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ B006
305 | : \
306 | """Docstring"""
|
= help: Replace with `None`; initialize within function

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