Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
965243d98e Remove Statements#depth 2023-08-07 12:11:50 -04:00
741 changed files with 49605 additions and 70738 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/*
- scripts/check_ecosystem.py
formatter:
- Cargo.toml
@@ -56,7 +56,6 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@@ -328,7 +327,7 @@ jobs:
name: "Formatter ecosystem and progress checks"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
if: needs.determine_changes.outputs.formatter == 'true'
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
@@ -338,6 +337,9 @@ jobs:
- name: "Formatter progress"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects
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

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@v3.0.2
uses: cloudflare/wrangler-action@2.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@v3.0.2
uses: cloudflare/wrangler-action@2.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -1,15 +1,5 @@
# 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,6 +131,7 @@ 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.
@@ -571,7 +572,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 --no-inline
flamegraph --perfdata perf.data
```
#### Mac

33
Cargo.lock generated
View File

@@ -14,18 +14,6 @@ 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"
@@ -812,7 +800,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.285"
version = "0.0.282"
dependencies = [
"anyhow",
"clap",
@@ -1003,16 +991,6 @@ 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"
@@ -2064,7 +2042,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.285"
version = "0.0.282"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2141,7 +2119,6 @@ dependencies = [
"ruff",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_index",
"ruff_python_parser",
"serde",
"serde_json",
@@ -2164,7 +2141,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.285"
version = "0.0.282"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2220,7 +2197,6 @@ dependencies = [
"anyhow",
"clap",
"ignore",
"imara-diff",
"indicatif",
"indoc",
"itertools",
@@ -2354,7 +2330,6 @@ dependencies = [
"similar",
"smallvec",
"thiserror",
"unicode-width",
]
[[package]]
@@ -2378,6 +2353,7 @@ dependencies = [
"is-macro",
"itertools",
"lexical-parse-float",
"num-bigint",
"num-traits",
"rand",
"unic-ucd-category",
@@ -2424,7 +2400,6 @@ dependencies = [
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_source_file",
"ruff_text_size",

View File

@@ -49,7 +49,6 @@ 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 [600 built-in rules](https://beta.ruff.rs/docs/rules/)
- 📏 Over [500 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.285
rev: v0.0.282
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.8
target-version = "py38"
# Assume Python 3.10.
target-version = "py310"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
@@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 500 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.285"
version = "0.0.282"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.285"
version = "0.0.282"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -62,6 +62,8 @@ 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 }
@@ -75,7 +77,7 @@ strum_macros = { workspace = true }
thiserror = { version = "1.0.43" }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode-width = { version = "0.1.10" }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
wsl = { version = "0.1.0" }

View File

@@ -14,19 +14,3 @@ 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,20 +68,6 @@ 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()):
...
@@ -275,32 +261,3 @@ 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,10 +74,3 @@ 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,19 +1,20 @@
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`
@@ -32,55 +33,3 @@ 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,13 +74,3 @@ 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,6 +1,7 @@
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]
@@ -8,9 +9,7 @@ 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]
@@ -18,14 +17,4 @@ 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
def func(arg: type[int, float] | str) -> None: ...

View File

@@ -1,12 +1,14 @@
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
@@ -14,11 +16,5 @@ 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,15 +80,3 @@ 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

@@ -1,4 +1,3 @@
from pickle import PicklingError, UnpicklingError
import socket
import pytest
@@ -21,12 +20,6 @@ def test_error_no_argument_given():
with pytest.raises(socket.error):
raise ValueError("Can't divide 1 by 0")
with pytest.raises(PicklingError):
raise PicklingError("Can't pickle")
with pytest.raises(UnpicklingError):
raise UnpicklingError("Can't unpickle")
def test_error_match_is_empty():
with pytest.raises(ValueError, match=None):

View File

@@ -1,26 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,12 +0,0 @@
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,7 +73,3 @@ print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._asdict())
import os
os._exit()

View File

@@ -1,31 +0,0 @@
## 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

@@ -1,12 +0,0 @@
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,14 +202,3 @@ 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

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

View File

@@ -21,29 +21,3 @@ 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,10 +30,3 @@ def foo() -> None:
if __name__ == "__main__":
import g
import h; import i
if __name__ == "__main__":
import j; \
import k

View File

@@ -1,16 +1,11 @@
# aaaa
# aaaaa
# a
# a
# aa
# aaa
# aaaa
# a
# aa
# aaa
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"""
if True: # noqa: E501
[12]
[12 ]
[1,2]
[1, 2]
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"""

View File

@@ -25,12 +25,6 @@ if (True) == TrueElement or x == TrueElement:
if res == True != False:
pass
if(True) == TrueElement or x == TrueElement:
pass
if (yield i) == True:
print("even")
#: Okay
if x not in y:
pass

View File

@@ -61,30 +61,3 @@ 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

@@ -133,8 +133,3 @@ def scope():
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
class TemperatureScales(Enum):
CELSIUS = (lambda deg_c: deg_c)
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)

View File

@@ -92,10 +92,3 @@ 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,13 +70,3 @@ 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,9 +145,3 @@ def f() -> None:
obj = Foo()
obj.do_thing()
def f():
try:
pass
except Exception as _:
pass

View File

@@ -1,46 +0,0 @@
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

@@ -14,10 +14,7 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
"{:*^30s}".format("centered")
## f-strings

View File

@@ -13,7 +13,6 @@ 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"
@@ -56,4 +55,3 @@ 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,4 +10,3 @@ os.getenv("AA", "GOOD" + "BAD")
os.getenv("AA", "GOOD" + 1)
os.getenv("AA", "GOOD %s" % "BAD")
os.getenv("B", Z)

View File

@@ -10,8 +10,6 @@ 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,10 +19,6 @@ 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,10 +15,6 @@ 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

@@ -1,13 +0,0 @@
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,30 +32,3 @@ 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,17 +15,3 @@ 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

@@ -0,0 +1,28 @@
# 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,42 +5,11 @@ from typing import TypeAlias
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 simple generic
# UP040 with generics (todo)
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

@@ -1,14 +0,0 @@
x = [1, 2, 3]
y = [4, 5, 6]
# RUF017
sum([x, y], start=[])
sum([x, y], [])
sum([[1, 2, 3], [4, 5, 6]], start=[])
sum([[1, 2, 3], [4, 5, 6]], [])
sum([[1, 2, 3], [4, 5, 6]],
[])
# OK
sum([x, y])
sum([[1, 2, 3], [4, 5, 6]])

View File

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

View File

@@ -52,7 +52,3 @@ 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,13 +179,16 @@ 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::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { 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;
@@ -209,7 +212,14 @@ 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

@@ -1,5 +1,4 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_ast::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -17,20 +16,14 @@ pub(crate) fn bindings(checker: &mut Checker) {
return;
}
for binding in &*checker.semantic.bindings {
for binding in checker.semantic.bindings.iter() {
if checker.enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception()
&& !binding.is_used()
&& !checker
.settings
.dummy_variable_rgx
.is_match(binding.name(checker.locator))
{
if binding.kind.is_bound_exception() && !binding.is_used() {
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),
},
binding.range(),
binding.range,
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {

View File

@@ -11,18 +11,21 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
let Stmt::For(ast::StmtFor {
if let Stmt::For(ast::StmtFor {
target, iter, body, ..
}) = 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);
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.current_statement()
{
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");
}
}
}

View File

@@ -1,5 +1,5 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::Ranged;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
@@ -82,7 +82,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range(),
binding.range,
));
}
}
@@ -123,14 +123,14 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
let line = checker.locator.compute_line_index(shadowed.range.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range(),
binding.range,
));
}
}
@@ -172,25 +172,18 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(statement_id) = shadowed.source else {
let Some(source) = 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;
}
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(checker.semantic.statement(source)),
&checker.semantic,
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
@@ -219,13 +212,13 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
let line = checker.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
},
binding.range(),
binding.range,
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
@@ -248,7 +241,10 @@ 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::Lambda(_)) {
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
@@ -274,7 +270,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | 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(expr) {
if pydocstyle::helpers::should_ignore_docstring(contents) {
#[allow(deprecated)]
let location = checker.locator.compute_source_location(expr.start());
warn_user!(

View File

@@ -80,9 +80,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// Avoid duplicate checks if the parent is an `Union[...]` since these rules
// traverse nested unions.
if !checker.semantic.in_nested_union() {
let is_unchecked_union = checker
.semantic
.current_expression_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.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
@@ -198,7 +206,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.current_scope().kind.is_function() {
if checker.semantic.current_scope().kind.is_any_function() {
// Ignore globals.
if !checker
.semantic
@@ -261,7 +269,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::load_before_global_declaration(checker, id, expr);
}
}
Expr::Attribute(attribute) => {
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) => {
// Ex) typing.List[...]
if checker.any_enabled(&[
Rule::FutureRewritableTypeAnnotation,
@@ -323,7 +331,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, attribute);
pandas_vet::rules::attr(checker, attr, value, expr);
}
Expr::Call(
call @ ast::ExprCall {
@@ -402,7 +410,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, call);
pyupgrade::rules::format_literals(checker, &summary, expr);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
@@ -418,7 +426,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker, val, location,
checker,
val.as_str(),
location,
);
}
}
@@ -673,8 +683,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker, expr, func, args, keywords,
);
}
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
if checker.enabled(Rule::BooleanPositionalValueInFunctionCall) {
flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call(
checker, args, func,
);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@@ -756,19 +768,9 @@ 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,
@@ -873,9 +875,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
}
Expr::Dict(ast::ExprDict {
keys,
@@ -924,7 +923,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::FString(ast::ExprFString { values, .. }) => {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@@ -951,7 +950,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
value: Constant::Str(value),
..
}) = left.as_ref()
{
@@ -1085,34 +1084,47 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
// Avoid duplicate checks if the parent is a union, since these rules already
// Avoid duplicate checks if the parent is an `|` since these rules
// traverse nested unions.
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);
}
let is_unchecked_union = !matches!(
checker.semantic.current_expression_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);
}
}
Expr::UnaryOp(
unary_op @ ast::ExprUnaryOp {
op,
operand,
range: _,
},
) => {
if checker.any_enabled(&[Rule::NotInTest, Rule::NotIsTest]) {
pycodestyle::rules::not_tests(checker, unary_op);
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,
);
}
if checker.enabled(Rule::UnaryPrefixIncrementDecrement) {
flake8_bugbear::rules::unary_prefix_increment_decrement(
@@ -1137,8 +1149,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
if checker.any_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) {
pycodestyle::rules::literal_comparisons(checker, compare);
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.enabled(Rule::IsLiteral) {
pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr);
@@ -1221,7 +1243,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
if let Some(diagnostic) = flake8_bandit::rules::hardcoded_tmp_directory(
expr,
value,
&checker.settings.flake8_bandit.hardcoded_tmp_directory,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());

View File

@@ -6,6 +6,9 @@ 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

@@ -69,18 +69,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
parameters,
body,
type_params,
range: _,
},
) => {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
}) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
}
@@ -145,11 +151,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) {
@@ -175,7 +181,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters);
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
parameters,
);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
@@ -206,9 +217,6 @@ 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,
@@ -300,7 +308,6 @@ 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);
}
@@ -310,16 +317,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
]) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::BooleanTypeHintPositionalArgument) {
flake8_boolean_trap::rules::boolean_type_hint_positional_argument(
if checker.enabled(Rule::BooleanPositionalArgInFunctionDefinition) {
flake8_boolean_trap::rules::check_positional_boolean_in_def(
checker,
name,
decorator_list,
parameters,
);
}
if checker.enabled(Rule::BooleanDefaultValuePositionalArgument) {
flake8_boolean_trap::rules::boolean_default_value_positional_argument(
if checker.enabled(Rule::BooleanDefaultValueInFunctionDefinition) {
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
checker,
name,
decorator_list,
@@ -512,16 +519,17 @@ 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);
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@@ -557,29 +565,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::BannedApi) {
flake8_tidy_imports::rules::banned_api(
flake8_tidy_imports::rules::name_or_parent_is_banned(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent {
module: &alias.name,
},
),
&alias,
&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);
@@ -694,7 +685,11 @@ 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);
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@@ -730,56 +725,16 @@ 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::banned_api(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
),
&stmt,
);
flake8_tidy_imports::rules::name_or_parent_is_banned(checker, &module, stmt);
for alias in names {
if &alias.name == "*" {
continue;
}
flake8_tidy_imports::rules::banned_api(
flake8_tidy_imports::rules::name_is_banned(
checker,
&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,
format!("{module}.{}", alias.name),
alias,
);
}
}
@@ -1142,7 +1097,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1152,7 +1108,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
with_,
stmt,
body,
checker.semantic.current_statement_parent(),
);
}
@@ -1177,6 +1134,13 @@ 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])
{
@@ -1226,7 +1190,14 @@ 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);
@@ -1368,7 +1339,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if !checker
.semantic
.current_scopes()
.any(|scope| scope.kind.is_function())
.any(|scope| scope.kind.is_any_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
@@ -1433,7 +1404,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if !checker
.semantic
.current_scopes()
.any(|scope| scope.kind.is_function())
.any(|scope| scope.kind.is_any_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(
checker, target, value, annotation,

View File

@@ -176,12 +176,13 @@ 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> {
if !self.semantic.in_f_string() {
let model = &self.semantic;
if !model.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = self.semantic.current_expression()?;
let expr = model.current_expression()?;
let string_range = self.indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
@@ -454,16 +455,22 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
parameters,
decorator_list,
type_params,
returns,
..
}) => {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@@ -524,7 +531,8 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function(function_def),
ExtractionTarget::Function,
stmt,
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -532,7 +540,8 @@ where
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
_ => unreachable!("Expected Stmt::FunctionDef"),
Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt),
_ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"),
});
self.deferred.functions.push(self.semantic.snapshot());
@@ -566,7 +575,8 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class(class_def),
ExtractionTarget::Class,
stmt,
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -599,7 +609,14 @@ 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) {
@@ -726,7 +743,8 @@ where
// Step 3: Clean-up
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
let scope_id = self.semantic.scope_id;
self.deferred.scopes.push(scope_id);
self.semantic.pop_scope(); // Function scope
@@ -1189,7 +1207,7 @@ where
));
}
}
Expr::FString(_) => {
Expr::JoinedStr(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@@ -1268,7 +1286,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::FString(ast::ExprFString { values, .. }) => {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
for value in values {
self.visit_expr(value);
}
@@ -1608,7 +1626,7 @@ impl<'a> Checker<'a> {
return;
}
if parent.is_for_stmt() {
if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) {
self.add_binding(
id,
expr.range(),
@@ -1807,14 +1825,19 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
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")
match &self.semantic.current_statement() {
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")
}
}
}
}
@@ -1855,7 +1878,7 @@ impl<'a> Checker<'a> {
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range())))
Some(names.iter().map(|name| (*name, binding.range)))
}
_ => None,
})

View File

@@ -1,10 +1,10 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_ast::Ranged;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_codegen::Stylist;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{

View File

@@ -94,15 +94,8 @@ pub(crate) fn check_noqa(
}
}
// 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()),
})
{
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(directive) => {

View File

@@ -226,10 +226,8 @@ 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
@@ -311,7 +309,6 @@ 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),
@@ -568,9 +565,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::BooleanTypeHintPositionalArgument),
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument),
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall),
(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),
// flake8-unused-arguments
(Flake8UnusedArguments, "001") => (RuleGroup::Unspecified, rules::flake8_unused_arguments::rules::UnusedFunctionArgument),
@@ -685,7 +682,6 @@ 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),
@@ -698,7 +694,6 @@ 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),
@@ -816,7 +811,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -1,6 +1,7 @@
//! 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.
@@ -27,48 +28,63 @@ 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) => docstring_from(member.body()),
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
}
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ExtractionTarget<'a> {
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
pub(crate) enum ExtractionTarget {
Class,
Function,
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub(crate) fn extract_definition<'a>(
target: ExtractionTarget<'a>,
target: ExtractionTarget,
stmt: &'a Stmt,
parent: DefinitionId,
definitions: &Definitions<'a>,
) -> Member<'a> {
match target {
ExtractionTarget::Function(function) => match &definitions[parent] {
ExtractionTarget::Function => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Function(function),
kind: MemberKind::Function,
stmt,
},
Definition::Member(Member {
kind: MemberKind::Class(_) | MemberKind::NestedClass(_),
kind: MemberKind::Class | MemberKind::NestedClass,
..
}) => Member {
parent,
kind: MemberKind::Method(function),
kind: MemberKind::Method,
stmt,
},
Definition::Member(_) => Member {
Definition::Member(..) => Member {
parent,
kind: MemberKind::NestedFunction(function),
kind: MemberKind::NestedFunction,
stmt,
},
},
ExtractionTarget::Class(class) => match &definitions[parent] {
Definition::Module(_) => Member {
ExtractionTarget::Class => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Class(class),
kind: MemberKind::Class,
stmt,
},
Definition::Member(_) => Member {
Definition::Member(..) => Member {
parent,
kind: MemberKind::NestedClass(class),
kind: MemberKind::NestedClass,
stmt,
},
},
}

View File

@@ -2,8 +2,9 @@ use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::{TextRange, TextSize};
use ruff_python_semantic::Definition;
use ruff_text_size::TextRange;
pub(crate) mod extraction;
pub(crate) mod google;
@@ -27,34 +28,43 @@ impl<'a> Docstring<'a> {
DocstringBody { docstring: self }
}
pub(crate) fn start(&self) -> TextSize {
self.expr.start()
}
pub(crate) fn end(&self) -> TextSize {
self.expr.end()
}
pub(crate) fn range(&self) -> TextRange {
self.expr.range()
}
pub(crate) fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
}
impl Ranged for Docstring<'_> {
fn range(&self) -> TextRange {
self.expr.range()
}
}
#[derive(Copy, Clone)]
pub(crate) struct DocstringBody<'a> {
docstring: &'a Docstring<'a>,
}
impl<'a> DocstringBody<'a> {
#[inline]
pub(crate) fn start(self) -> TextSize {
self.range().start()
}
pub(crate) fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
pub(crate) fn as_str(self) -> &'a str {
&self.docstring.contents[self.docstring.body_range]
}
}
impl Ranged for DocstringBody<'_> {
fn range(&self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
}
impl Deref for DocstringBody<'_> {
type Target = str;

View File

@@ -2,7 +2,6 @@ use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
@@ -367,12 +366,6 @@ impl<'a> SectionContext<'a> {
}
}
impl Ranged for SectionContext<'_> {
fn range(&self) -> TextRange {
self.range()
}
}
impl Debug for SectionContext<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SectionContext")

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::Absolutize;
use path_absolutize::{path_dedot, Absolutize};
use crate::registry::RuleSet;
@@ -61,13 +61,7 @@ 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();
#[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) {
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
return format!("{}", path.display());
}
format!("{}", path.display())

View File

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

View File

@@ -194,7 +194,7 @@ impl<'a> Importer<'a> {
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the start of the file, since it could
// be shadowed between the import and the current location.
if imported_name.start() > at {
if imported_name.range().start() > at {
return Some(Err(ResolutionError::ImportAfterUsage));
}

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::fmt::Display;
use std::fs::File;
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
use std::io::{BufReader, BufWriter, 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::from_path(path).map_err(|err| {
let mut notebook = Notebook::read(path).map_err(|err| {
anyhow::anyhow!(
"Failed to read notebook file `{}`: {:?}",
path.display(),
@@ -120,30 +120,18 @@ pub struct Notebook {
impl Notebook {
/// Read the Jupyter Notebook from the given [`Path`].
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
///
/// 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| {
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')
@@ -156,7 +144,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -171,19 +159,14 @@ 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 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(),
)
})?;
let contents = std::fs::read_to_string(path).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(
@@ -199,7 +182,7 @@ impl Notebook {
Diagnostic::new(
SyntaxError {
message: format!(
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT} extension), \
which must be internally stored as JSON, \
but found a Python source file: {err}"
),
@@ -501,22 +484,22 @@ mod tests {
fn test_invalid() {
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
assert_eq!(
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
Notebook::read(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \
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::from_path(path).unwrap_err().kind.body,
Notebook::read(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::from_path(path).unwrap_err().kind.body,
Notebook::read(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"
);
@@ -588,11 +571,11 @@ print("after empty cells")
}
#[test]
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("ipy_escape_command_expected.ipynb"),
Path::new("line_magics_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
---
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |

View File

@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
@@ -59,7 +58,7 @@ impl Eq for LineWidth {}
impl PartialOrd for LineWidth {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
self.width.partial_cmp(&other.width)
}
}
@@ -84,7 +83,7 @@ impl LineWidth {
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.as_usize();
let tab_size: usize = self.tab_size.into();
for c in chars {
match c {
'\t' => {
@@ -145,22 +144,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(NonZeroU8);
impl TabSize {
pub(crate) fn as_usize(self) -> usize {
self.0.get() as usize
}
}
pub struct TabSize(pub u8);
impl Default for TabSize {
fn default() -> Self {
Self(NonZeroU8::new(4).unwrap())
Self(4)
}
}
impl From<NonZeroU8> for TabSize {
fn from(tab_size: NonZeroU8) -> Self {
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

View File

@@ -293,10 +293,12 @@ 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(TabSize::default());
let mut line_width = LineWidth::new(TAB_SIZE);
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::BooleanPositionalValueInCall,
/// Rule::BooleanPositionalValueInFunctionCall,
/// ]);
///
/// 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::BooleanPositionalValueInCall));
/// assert!(union.contains(Rule::BooleanPositionalValueInFunctionCall));
/// ```
#[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::BooleanPositionalValueInCall,
/// Rule::BooleanPositionalValueInFunctionCall,
/// Rule::BadQuotesInlineString
/// ])));
/// ```

View File

@@ -4,7 +4,6 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_ast::Ranged;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
pub(crate) struct Renamer;
@@ -221,12 +220,12 @@ impl Renamer {
BindingKind::Import(_) | BindingKind::FromImport(_) => {
if binding.is_alias() {
// Ex) Rename `import pandas as alias` to `import pandas as pd`.
Some(Edit::range_replacement(target.to_string(), binding.range()))
Some(Edit::range_replacement(target.to_string(), binding.range))
} else {
// Ex) Rename `import pandas` to `import pandas as pd`.
Some(Edit::range_replacement(
format!("{name} as {target}"),
binding.range(),
binding.range,
))
}
}
@@ -235,7 +234,7 @@ impl Renamer {
let module_name = import.call_path.first().unwrap();
Some(Edit::range_replacement(
format!("{module_name} as {target}"),
binding.range(),
binding.range,
))
}
// Avoid renaming builtins and other "special" bindings.
@@ -255,7 +254,7 @@ impl Renamer {
| BindingKind::FunctionDefinition(_)
| BindingKind::Deletion
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range()))
Some(Edit::range_replacement(target.to_string(), binding.range))
}
}
}

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(ast::StringConstant { value, .. }) => value,
Constant::Str(value) => value,
_ => return None,
},
_ => return None,

View File

@@ -1,25 +1,25 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
pub(crate) fn add_return_annotation(
locator: &Locator,
stmt: &Stmt,
annotation: &str,
source_type: PySourceType,
locator: &Locator,
) -> Result<Edit> {
let contents = &locator.contents()[statement.range()];
let contents = &locator.contents()[stmt.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(), statement.start()).flatten()
lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten()
{
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {

View File

@@ -1,11 +1,53 @@
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, SemanticModel};
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"),
}
}
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option<String> {
let function = definition.as_function_def()?;
if visibility::is_overload(&function.decorator_list, semantic) {
Some(function.name.to_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
}
} else {
None
}
@@ -18,12 +60,19 @@ pub(crate) fn is_overload_impl(
overloaded_name: &str,
semantic: &SemanticModel,
) -> bool {
let Some(function) = definition.as_function_def() else {
return false;
};
if visibility::is_overload(&function.decorator_list, semantic) {
false
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 {
function.name.as_str() == overloaded_name
false
}
}

View File

@@ -1,19 +1,23 @@
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;
use ruff_python_semantic::{Definition, Member, MemberKind};
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.
///
@@ -494,23 +498,20 @@ pub(crate) fn definition(
definition: &Definition,
visibility: visibility::Visibility,
) -> Vec<Diagnostic> {
let Some(function) = definition.as_function_def() else {
// 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 {
return vec![];
};
let ast::StmtFunctionDef {
range: _,
is_async: _,
decorator_list,
name,
type_params: _,
parameters,
returns,
body,
} = function;
let is_method = definition.is_method();
let is_method = match kind {
MemberKind::Method => true,
MemberKind::Function | MemberKind::NestedFunction => false,
_ => return vec![],
};
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?
@@ -527,19 +528,20 @@ pub(crate) fn definition(
parameter,
default: _,
range: _,
} in parameters
} in arguments
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()),
is_method
&& !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()),
),
)
{
// ANN401 for dynamically typed parameters
// ANN401 for dynamically typed arguments
if let Some(annotation) = &parameter.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) && !is_overridden {
@@ -570,7 +572,7 @@ pub(crate) fn definition(
}
// ANN002, ANN401
if let Some(arg) = &parameters.vararg {
if let Some(arg) = &arguments.vararg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@@ -596,7 +598,7 @@ pub(crate) fn definition(
}
// ANN003, ANN401
if let Some(arg) = &parameters.kwarg {
if let Some(arg) = &arguments.kwarg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@@ -627,18 +629,18 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) {
if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) {
if let Some(ParameterWithDefault {
parameter,
default: _,
range: _,
}) = parameters
}) = arguments
.posonlyargs
.first()
.or_else(|| parameters.args.first())
.or_else(|| arguments.args.first())
{
if parameter.annotation.is_none() {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
@@ -674,22 +676,24 @@ pub(crate) fn definition(
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
} else if is_method
&& visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic())
{
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
} else if is_method && visibility::is_init(name) {
@@ -701,15 +705,15 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
checker.locator(),
stmt,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
@@ -723,16 +727,16 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
function.identifier(),
stmt.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(
function,
checker.locator(),
stmt,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
@@ -748,7 +752,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
}
@@ -758,7 +762,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
}

View File

@@ -112,21 +112,21 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
fn int_value(expr: &Expr, model: &SemanticModel) -> Option<u16> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_u16(),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
}) => {
let left_value = int_value(left, semantic)?;
let right_value = int_value(right, semantic)?;
let left_value = int_value(left, model)?;
let right_value = int_value(right, model)?;
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, semantic: &SemanticModel) -> bool {
fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool {
match expr {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
@@ -62,7 +62,7 @@ fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bo
..
}) => {
// Only evaluate the full BinOp, not the nested components.
if semantic
if model
.current_expression_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
@@ -80,7 +80,7 @@ fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bo
attr == "format" && string_literal(value).is_some()
}
// f"select * from table where val = {val}"
Expr::FString(_) => true,
Expr::JoinedStr(_) => true,
_ => false,
}
}

View File

@@ -1,10 +1,8 @@
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_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.
///
@@ -51,33 +49,19 @@ impl Violation for HardcodedTempFile {
}
/// S108
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;
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
}
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

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

View File

@@ -1,4 +1,8 @@
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
@@ -58,13 +62,18 @@ pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
false
}
/// Returns `true` if an expression is a boolean literal.
pub(super) const fn is_boolean(expr: &Expr) -> bool {
const fn is_boolean_arg(arg: &Expr) -> bool {
matches!(
&expr,
&arg,
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::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValuePositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInCall, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInFunctionCall, 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

@@ -1,126 +0,0 @@
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

@@ -0,0 +1,90 @@
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,9 +1,11 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_python_ast::Expr;
use ruff_diagnostics::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::{allow_boolean_trap, is_boolean};
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_trap};
/// ## What it does
/// Checks for boolean positional arguments in function calls.
@@ -15,42 +17,44 @@ use crate::rules::flake8_boolean_trap::helpers::{allow_boolean_trap, is_boolean}
///
/// ## Example
/// ```python
/// def func(flag: bool) -> None:
/// def foo(flag: bool) -> None:
/// ...
///
///
/// func(True)
/// foo(True)
/// ```
///
/// Use instead:
/// ```python
/// def func(flag: bool) -> None:
/// def foo(flag: bool) -> None:
/// ...
///
///
/// func(flag=True)
/// foo(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 BooleanPositionalValueInCall;
pub struct BooleanPositionalValueInFunctionCall;
impl Violation for BooleanPositionalValueInCall {
impl Violation for BooleanPositionalValueInFunctionCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional value in function call")
}
}
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
pub(crate) fn check_boolean_positional_value_in_function_call(
checker: &mut Checker,
args: &[Expr],
func: &Expr,
) {
if allow_boolean_trap(func) {
return;
}
for arg in args.iter().filter(|arg| is_boolean(arg)) {
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));
for arg in args {
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
}
}

View File

@@ -11,22 +11,16 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a `bool` type hint.
/// Checks for boolean positional arguments in function definitions.
///
/// ## 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
@@ -71,33 +65,20 @@ 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 BooleanTypeHintPositionalArgument;
pub struct BooleanPositionalArgInFunctionDefinition;
impl Violation for BooleanTypeHintPositionalArgument {
impl Violation for BooleanPositionalArgInFunctionDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean-typed positional argument in function definition")
format!("Boolean positional arg in function definition")
}
}
pub(crate) fn boolean_type_hint_positional_argument(
pub(crate) fn check_positional_boolean_in_def(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
@@ -120,25 +101,28 @@ pub(crate) fn boolean_type_hint_positional_argument(
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
let Some(annotation) = parameter.annotation.as_ref() else {
if parameter.annotation.is_none() {
continue;
}
let Some(expr) = &parameter.annotation else {
continue;
};
// check for both bool (python class) and 'bool' (string annotation)
let hint = match annotation.as_ref() {
let hint = match expr.as_ref() {
Expr::Name(name) => &name.id == "bool",
Expr::Constant(ast::ExprConstant {
value: Constant::Str(ast::StringConstant { value, .. }),
value: Constant::Str(value),
..
}) => value == "bool",
_ => false,
};
if !hint || !checker.semantic().is_builtin("bool") {
if !hint {
continue;
}
checker.diagnostics.push(Diagnostic::new(
BooleanTypeHintPositionalArgument,
parameter.name.range(),
BooleanPositionalArgInFunctionDefinition,
parameter.range(),
));
}
}

View File

@@ -1,7 +1,7 @@
pub(crate) use boolean_default_value_positional_argument::*;
pub(crate) use boolean_positional_value_in_call::*;
pub(crate) use boolean_type_hint_positional_argument::*;
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::*;
mod boolean_default_value_positional_argument;
mod boolean_positional_value_in_call;
mod 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;

View File

@@ -1,91 +1,91 @@
---
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
FBT.py:4:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:5:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:10:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:11:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:14:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:15:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:18:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:19:5: FBT001 Boolean positional arg 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-typed positional argument in function definition
FBT.py:86:19: FBT001 Boolean positional arg 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:5: FBT002 Boolean default positional argument in function definition
FBT.py:12:31: FBT002 Boolean default value 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:5: FBT002 Boolean default positional argument in function definition
FBT.py:13:43: FBT002 Boolean default value 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:5: FBT002 Boolean default positional argument in function definition
FBT.py:14:41: FBT002 Boolean default value 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:5: FBT002 Boolean default positional argument in function definition
FBT.py:15:46: FBT002 Boolean default value 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,6 +49,7 @@ 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(
@@ -59,17 +60,6 @@ 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();
@@ -82,7 +72,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
..Settings::for_rules(vec![Rule::FunctionCallInDefaultArgument])
},
)?;
assert_messages!(snapshot, diagnostics);

View File

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

View File

@@ -150,9 +150,7 @@ fn duplicate_handler_exceptions<'a>(
if unique_elts.len() == 1 {
checker.generator().expr(unique_elts[0])
} else {
// 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)))
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_f_string_expr() {
if !value.is_joined_str_expr() {
return;
}
checker

View File

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

View File

@@ -86,6 +86,9 @@ 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();
@@ -233,7 +236,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 stmt.is_function_def_stmt() {
if matches!(stmt, Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_)) {
// Don't recurse.
return;
}
@@ -248,7 +251,8 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign { target, .. })
| Stmt::For(ast::StmtFor { target, .. }) => {
| Stmt::For(ast::StmtFor { target, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { target, .. }) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
self.names.extend(visitor.names);

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