Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d0113480c | ||
|
|
389fe13c93 | ||
|
|
3000a47fe8 | ||
|
|
cbf6085375 | ||
|
|
9171bd4c28 | ||
|
|
f5c69c1b34 | ||
|
|
5f63b8bfb8 | ||
|
|
c996b614fe | ||
|
|
670db1db4b | ||
|
|
242cbd966d | ||
|
|
e7f228f781 | ||
|
|
51d8fc1f30 | ||
|
|
ed72c027a3 | ||
|
|
b7e7346081 | ||
|
|
d35b5248ea | ||
|
|
c535e10fff | ||
|
|
c3ecdb8783 | ||
|
|
242df67cbf | ||
|
|
776d598738 | ||
|
|
7f3797185c | ||
|
|
a9f535997d | ||
|
|
fdb3c8852f | ||
|
|
42d969f19f | ||
|
|
62ffc773de | ||
|
|
6feb3fcc1b | ||
|
|
8eadacda33 | ||
|
|
8a7dcb794b | ||
|
|
dfa81b6fe0 | ||
|
|
33196f1859 | ||
|
|
0d94337b96 | ||
|
|
f9726af4ef | ||
|
|
727153cf45 | ||
|
|
574c0e0105 | ||
|
|
700c816fd5 | ||
|
|
3b56f6d616 | ||
|
|
110fa804ff | ||
|
|
2b9c22de0f | ||
|
|
51ebff7e41 | ||
|
|
742f615792 | ||
|
|
95e6258d5d | ||
|
|
5dbb4dd823 | ||
|
|
46f8961292 | ||
|
|
f886b58c92 | ||
|
|
057faabcdd | ||
|
|
0bb175f7f6 | ||
|
|
4b2ec7d562 | ||
|
|
4aac801277 | ||
|
|
45a24912a6 | ||
|
|
3914fcb7ca | ||
|
|
6d58b773b1 | ||
|
|
e7f5121922 | ||
|
|
1776cbd2e2 | ||
|
|
71f1643eda | ||
|
|
74dc137b30 | ||
|
|
97e31cad2f | ||
|
|
ed7d2b8a3d | ||
|
|
c7e4c58181 | ||
|
|
6ff566f2c1 | ||
|
|
32773e8309 | ||
|
|
050f5953f8 | ||
|
|
aba340a177 | ||
|
|
33657d3a1c | ||
|
|
45318d08b7 | ||
|
|
86b6a3e1ad | ||
|
|
f5a2fb5b5d | ||
|
|
94a004ee9c | ||
|
|
aaf7f362a1 | ||
|
|
2dcd9e2e9c | ||
|
|
40e9884353 | ||
|
|
9bbb0a5151 |
11
.github/workflows/ci.yaml
vendored
11
.github/workflows/ci.yaml
vendored
@@ -319,8 +319,8 @@ jobs:
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: mkdocs build --strict -f mkdocs.generated.yml
|
||||
|
||||
check-formatter-stability:
|
||||
name: "Check formatter stability"
|
||||
check-formatter-ecosystem:
|
||||
name: "Formatter ecosystem and progress checks"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.formatter == 'true'
|
||||
@@ -330,7 +330,12 @@ jobs:
|
||||
run: rustup show
|
||||
- name: "Cache rust"
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: "Formatter progress"
|
||||
run: scripts/formatter_progress.sh
|
||||
- name: "Github step summary"
|
||||
run: grep "similarity index" target/progress_projects_report.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 stability"
|
||||
- name: "Check CPython stability"
|
||||
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython
|
||||
|
||||
4080
CHANGELOG.md
Normal file
4080
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
|
||||
|
||||
### Development
|
||||
|
||||
After cloning the repository, run Ruff locally with:
|
||||
After cloning the repository, run Ruff locally from the repository root with:
|
||||
|
||||
```shell
|
||||
cargo run -p ruff_cli -- check /path/to/file.py --no-cache
|
||||
@@ -156,10 +156,13 @@ At a high level, the steps involved in adding a new lint rule are as follows:
|
||||
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
|
||||
an `ast::StmtAssert` node).
|
||||
|
||||
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for
|
||||
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
|
||||
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
|
||||
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
|
||||
1. Define the logic for invoking the diagnostic in `crates/ruff/src/checkers/ast/analyze` (for
|
||||
AST-based rules), `crates/ruff/src/checkers/tokens.rs` (for token-based rules),
|
||||
`crates/ruff/src/checkers/physical_lines.rs` (for text-based rules),
|
||||
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based rules), etc. For AST-based rules,
|
||||
you'll likely want to modify `analyze/statement.rs` (if your rule is based on analyzing
|
||||
statements, like imports) or `analyze/expression.rs` (if your rule is based on analyzing
|
||||
expressions, like function calls).
|
||||
|
||||
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`).
|
||||
|
||||
|
||||
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -734,7 +734,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.279"
|
||||
version = "0.0.280"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1295,12 +1295,6 @@ dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -1524,7 +1518,6 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
@@ -1548,19 +1541,6 @@ dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
@@ -1888,7 +1868,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.279"
|
||||
version = "0.0.280"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1909,14 +1889,12 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"natord",
|
||||
"nohash-hasher",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs",
|
||||
"phf",
|
||||
"pretty_assertions",
|
||||
"pyproject-toml",
|
||||
"quick-junit",
|
||||
@@ -1988,7 +1966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.279"
|
||||
version = "0.0.280"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2017,6 +1995,7 @@ dependencies = [
|
||||
"ruff",
|
||||
"ruff_cache",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_stdlib",
|
||||
@@ -2179,7 +2158,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.3.3",
|
||||
"is-macro",
|
||||
"nohash-hasher",
|
||||
"num-traits",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
|
||||
@@ -26,7 +26,6 @@ is-macro = { version = "0.2.2" }
|
||||
itertools = { version = "0.10.5" }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = "2.5.0"
|
||||
nohash-hasher = { version = "0.2.0" }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-traits = { version = "0.2.15" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
|
||||
@@ -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.279
|
||||
rev: v0.0.280
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.279"
|
||||
version = "0.0.280"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.279"
|
||||
version = "0.0.280"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -45,7 +45,6 @@ libcst = { workspace = true }
|
||||
log = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
natord = { version = "1.0.9" }
|
||||
nohash-hasher = { workspace = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
@@ -55,7 +54,6 @@ path-absolutize = { workspace = true, features = [
|
||||
] }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.3.1", features = ["serde"] }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
pyproject-toml = { version = "0.6.0" }
|
||||
quick-junit = { version = "0.3.2" }
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Should emit:
|
||||
B002 - on lines 15 and 20
|
||||
B002 - on lines 18, 19, and 24
|
||||
"""
|
||||
|
||||
|
||||
@@ -8,13 +8,17 @@ def this_is_all_fine(n):
|
||||
x = n + 1
|
||||
y = 1 + n
|
||||
z = +x + y
|
||||
return +z
|
||||
a = n - 1
|
||||
b = 1 - n
|
||||
c = -a - b
|
||||
return +z, -c
|
||||
|
||||
|
||||
def this_is_buggy(n):
|
||||
x = ++n
|
||||
return x
|
||||
y = --n
|
||||
return x, y
|
||||
|
||||
|
||||
def this_is_buggy_too(n):
|
||||
return ++n
|
||||
return ++n, --n
|
||||
|
||||
@@ -17,3 +17,37 @@ from typing import TypedDict
|
||||
|
||||
class MyClass(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
from threading import Event
|
||||
|
||||
|
||||
class CustomEvent(Event):
|
||||
def set(self) -> None:
|
||||
...
|
||||
|
||||
def str(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
from logging import Filter, LogRecord
|
||||
|
||||
|
||||
class CustomFilter(Filter):
|
||||
def filter(self, record: LogRecord) -> bool:
|
||||
...
|
||||
|
||||
def str(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class MyClass:
|
||||
@override
|
||||
def str(self):
|
||||
pass
|
||||
|
||||
def int(self):
|
||||
pass
|
||||
|
||||
2
crates/ruff/resources/test/fixtures/flake8_executable/EXE004_4.py
vendored
Executable file
2
crates/ruff/resources/test/fixtures/flake8_executable/EXE004_4.py
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
|
||||
#!/usr/bin/env python
|
||||
@@ -1,5 +1,10 @@
|
||||
import logging
|
||||
from distutils import log
|
||||
|
||||
from logging_setup import logger
|
||||
|
||||
logging.warn("Hello World!")
|
||||
log.warn("Hello world!") # This shouldn't be considered as a logger candidate
|
||||
logger.warn("Hello world!")
|
||||
|
||||
logging . warn("Hello World!")
|
||||
|
||||
12
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.py
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
__all__ = ["A", "B", "C"]
|
||||
|
||||
# Errors
|
||||
__all__.append("D")
|
||||
__all__.extend(["E", "Foo"])
|
||||
__all__.remove("A")
|
||||
|
||||
# OK
|
||||
__all__ += ["D"]
|
||||
foo = ["Hello"]
|
||||
foo.append("World")
|
||||
foo.bar.append("World")
|
||||
12
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi
vendored
Normal file
12
crates/ruff/resources/test/fixtures/flake8_pyi/PYI056.pyi
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
__all__ = ["A", "B", "C"]
|
||||
|
||||
# Errors
|
||||
__all__.append("D")
|
||||
__all__.extend(["E", "Foo"])
|
||||
__all__.remove("A")
|
||||
|
||||
# OK
|
||||
__all__ += ["D"]
|
||||
foo = ["Hello"]
|
||||
foo.append("World")
|
||||
foo.bar.append("World")
|
||||
@@ -14,6 +14,12 @@ try:
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except (ValueError, OSError) as e:
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
@@ -94,3 +100,13 @@ def with_comment():
|
||||
foo()
|
||||
except (ValueError, OSError):
|
||||
pass # Trailing comment.
|
||||
|
||||
try:
|
||||
print()
|
||||
except ("not", "an", "exception"):
|
||||
pass
|
||||
|
||||
try:
|
||||
print()
|
||||
except "not an exception":
|
||||
pass
|
||||
|
||||
@@ -101,3 +101,16 @@ if a:
|
||||
x = 1
|
||||
elif c:
|
||||
x = 1
|
||||
|
||||
|
||||
def foo():
|
||||
a = True
|
||||
b = False
|
||||
if a > b: # end-of-line
|
||||
return 3
|
||||
elif a == b:
|
||||
return 3
|
||||
elif a < b: # end-of-line
|
||||
return 4
|
||||
elif b is None:
|
||||
return 4
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
key in obj.keys() # SIM118
|
||||
|
||||
key not in obj.keys() # SIM118
|
||||
|
||||
foo["bar"] in obj.keys() # SIM118
|
||||
|
||||
foo["bar"] not in obj.keys() # SIM118
|
||||
|
||||
foo['bar'] in obj.keys() # SIM118
|
||||
|
||||
foo['bar'] not in obj.keys() # SIM118
|
||||
|
||||
foo() in obj.keys() # SIM118
|
||||
|
||||
foo() not in obj.keys() # SIM118
|
||||
|
||||
for key in obj.keys(): # SIM118
|
||||
pass
|
||||
|
||||
|
||||
20
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH206.py
vendored
Normal file
20
crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH206.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from os import sep
|
||||
|
||||
|
||||
file_name = "foo/bar"
|
||||
|
||||
# PTH206
|
||||
"foo/bar/".split(os.sep)
|
||||
"foo/bar/".split(sep=os.sep)
|
||||
"foo/bar/".split(os.sep)[-1]
|
||||
"foo/bar/".split(os.sep)[-2]
|
||||
"foo/bar/".split(os.sep)[-2:]
|
||||
"fizz/buzz".split(sep)
|
||||
"fizz/buzz".split(sep)[-1]
|
||||
os.path.splitext("path/to/hello_world.py")[0].split(os.sep)[-1]
|
||||
file_name.split(os.sep)
|
||||
(os.path.abspath(file_name)).split(os.sep)
|
||||
|
||||
# OK
|
||||
"foo/bar/".split("/")
|
||||
7
crates/ruff/resources/test/fixtures/isort/if_elif_else.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/isort/if_elif_else.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
if "sdist" in cmds:
|
||||
_sdist = cmds["sdist"]
|
||||
elif "setuptools" in sys.modules:
|
||||
from setuptools.command.sdist import sdist as _sdist
|
||||
else:
|
||||
from setuptools.command.sdist import sdist as _sdist
|
||||
from distutils.command.sdist import sdist as _sdist
|
||||
7
crates/ruff/resources/test/fixtures/isort/match_case.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/isort/match_case.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
match 1:
|
||||
case 1:
|
||||
import sys
|
||||
import os
|
||||
case 2:
|
||||
import collections
|
||||
import abc
|
||||
@@ -41,3 +41,5 @@ regex = '\w' # noqa
|
||||
regex = '''
|
||||
\w
|
||||
''' # noqa
|
||||
|
||||
regex = '\\\_'
|
||||
|
||||
@@ -23,3 +23,5 @@ a = []
|
||||
'%s %s' % (*a,)
|
||||
k = {}
|
||||
'%(k)s' % {**k}
|
||||
'%s' % [1, 2, 3]
|
||||
'%s' % {1, 2, 3}
|
||||
|
||||
11
crates/ruff/resources/test/fixtures/pyflakes/F821_16.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/pyflakes/F821_16.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Test case: `Literal` with `__future__` annotations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Final
|
||||
|
||||
from typing_extensions import assert_type
|
||||
|
||||
CONSTANT: Final = "ns"
|
||||
|
||||
assert_type(CONSTANT, Literal["ns"])
|
||||
@@ -39,3 +39,27 @@ class Class:
|
||||
def f(self):
|
||||
print(my_var)
|
||||
my_var = 1
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print(sys.argv)
|
||||
|
||||
try:
|
||||
3 / 0
|
||||
except ZeroDivisionError:
|
||||
import sys
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print(sys.argv)
|
||||
|
||||
for sys in range(5):
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
x = 1 # type: ignore
|
||||
x = 1 # type:ignore
|
||||
x = 1 # type: ignore[attr-defined] # type: ignore
|
||||
x = 1 # type: ignoreme # type: ignore
|
||||
|
||||
x = 1
|
||||
x = 1 # type ignore
|
||||
|
||||
41
crates/ruff/resources/test/fixtures/pylint/self_assigning_variable.py
vendored
Normal file
41
crates/ruff/resources/test/fixtures/pylint/self_assigning_variable.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
foo = 1
|
||||
bar = 2
|
||||
baz = 3
|
||||
|
||||
# Errors.
|
||||
foo = foo
|
||||
bar = bar
|
||||
foo, bar = foo, bar
|
||||
bar, foo = bar, foo
|
||||
(foo, bar) = (foo, bar)
|
||||
(bar, foo) = (bar, foo)
|
||||
foo, (bar, baz) = foo, (bar, baz)
|
||||
bar, (foo, baz) = bar, (foo, baz)
|
||||
(foo, bar), baz = (foo, bar), baz
|
||||
(foo, (bar, baz)) = (foo, (bar, baz))
|
||||
foo, bar = foo, 1
|
||||
bar, foo = bar, 1
|
||||
(foo, bar) = (foo, 1)
|
||||
(bar, foo) = (bar, 1)
|
||||
foo, (bar, baz) = foo, (bar, 1)
|
||||
bar, (foo, baz) = bar, (foo, 1)
|
||||
(foo, bar), baz = (foo, bar), 1
|
||||
(foo, (bar, baz)) = (foo, (bar, 1))
|
||||
foo: int = foo
|
||||
bar: int = bar
|
||||
|
||||
# Non-errors.
|
||||
foo = bar
|
||||
bar = foo
|
||||
foo, bar = bar, foo
|
||||
foo, bar = bar, foo
|
||||
(foo, bar) = (bar, foo)
|
||||
foo, bar = bar, 1
|
||||
bar, foo = foo, 1
|
||||
foo: int = bar
|
||||
bar: int = 1
|
||||
|
||||
|
||||
class Foo:
|
||||
foo = foo
|
||||
bar = bar
|
||||
18
crates/ruff/resources/test/fixtures/pylint/subprocess_popen_preexec_fn.py
vendored
Normal file
18
crates/ruff/resources/test/fixtures/pylint/subprocess_popen_preexec_fn.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
# Errors.
|
||||
subprocess.Popen(preexec_fn=foo)
|
||||
subprocess.Popen(["ls"], preexec_fn=foo)
|
||||
subprocess.Popen(preexec_fn=lambda: print("Hello, world!"))
|
||||
subprocess.Popen(["ls"], preexec_fn=lambda: print("Hello, world!"))
|
||||
|
||||
# Non-errors.
|
||||
subprocess.Popen()
|
||||
subprocess.Popen(["ls"])
|
||||
subprocess.Popen(preexec_fn=None) # None is the default.
|
||||
subprocess.Popen(["ls"], preexec_fn=None) # None is the default.
|
||||
@@ -15,7 +15,22 @@ bytes("foo", **a)
|
||||
bytes(b"foo"
|
||||
b"bar")
|
||||
bytes("foo")
|
||||
bytes(1)
|
||||
f"{f'{str()}'}"
|
||||
int(1.0)
|
||||
int("1")
|
||||
int(b"11")
|
||||
int(10, base=2)
|
||||
int("10", base=2)
|
||||
int("10", 2)
|
||||
float("1.0")
|
||||
float(b"1.0")
|
||||
bool(1)
|
||||
bool(0)
|
||||
bool("foo")
|
||||
bool("")
|
||||
bool(b"")
|
||||
bool(1.0)
|
||||
|
||||
# These become string or byte literals
|
||||
str()
|
||||
@@ -27,3 +42,10 @@ bytes(b"foo")
|
||||
bytes(b"""
|
||||
foo""")
|
||||
f"{str()}"
|
||||
int()
|
||||
int(1)
|
||||
float()
|
||||
float(1.0)
|
||||
bool()
|
||||
bool(True)
|
||||
bool(False)
|
||||
|
||||
@@ -124,6 +124,22 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
111111
|
||||
)
|
||||
|
||||
"{}".format(
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
)
|
||||
|
||||
"{a}".format(
|
||||
a=[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
)
|
||||
|
||||
async def c():
|
||||
return "{}".format(await 3)
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use nohash_hasher::IntSet;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
@@ -47,7 +46,7 @@ fn apply_fixes<'a>(
|
||||
let mut output = String::with_capacity(locator.len());
|
||||
let mut last_pos: Option<TextSize> = None;
|
||||
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
|
||||
let mut isolated: IntSet<u32> = IntSet::default();
|
||||
let mut isolated: FxHashSet<u32> = FxHashSet::default();
|
||||
let mut fixed = FxHashMap::default();
|
||||
let mut source_map = SourceMap::default();
|
||||
|
||||
|
||||
27
crates/ruff/src/checkers/ast/analyze/argument.rs
Normal file
27
crates/ruff/src/checkers/ast/analyze/argument.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use rustpython_parser::ast::{Arg, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
|
||||
|
||||
/// Run lint rules over an [`Arg`] syntax node.
|
||||
pub(crate) fn argument(arg: &Arg, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) = pycodestyle::rules::ambiguous_variable_name(&arg.arg, arg.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::InvalidArgumentName) {
|
||||
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
|
||||
&arg.arg,
|
||||
arg,
|
||||
&checker.settings.pep8_naming.ignore_names,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::BuiltinArgumentShadowing) {
|
||||
flake8_builtins::rules::builtin_argument_shadowing(checker, arg);
|
||||
}
|
||||
}
|
||||
26
crates/ruff/src/checkers/ast/analyze/arguments.rs
Normal file
26
crates/ruff/src/checkers/ast/analyze/arguments.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use rustpython_parser::ast::Arguments;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
|
||||
|
||||
/// Run lint rules over a [`Arguments`] syntax node.
|
||||
pub(crate) fn arguments(arguments: &Arguments, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::MutableArgumentDefault) {
|
||||
flake8_bugbear::rules::mutable_argument_default(checker, arguments);
|
||||
}
|
||||
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
|
||||
flake8_bugbear::rules::function_call_in_argument_default(checker, arguments);
|
||||
}
|
||||
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
|
||||
ruff::rules::implicit_optional(checker, arguments);
|
||||
}
|
||||
if checker.is_stub {
|
||||
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
|
||||
flake8_pyi::rules::typed_argument_simple_defaults(checker, arguments);
|
||||
}
|
||||
if checker.enabled(Rule::ArgumentDefaultInStub) {
|
||||
flake8_pyi::rules::argument_simple_defaults(checker, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
crates/ruff/src/checkers/ast/analyze/bindings.rs
Normal file
68
crates/ruff/src/checkers/ast/analyze/bindings.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
|
||||
/// Run lint rules over the [`Binding`]s.
|
||||
pub(crate) fn bindings(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::InvalidAllFormat,
|
||||
Rule::InvalidAllObject,
|
||||
Rule::UnaliasedCollectionsAbcSetImport,
|
||||
Rule::UnconventionalImportAlias,
|
||||
Rule::UnusedVariable,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for binding in checker.semantic.bindings.iter() {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
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,
|
||||
);
|
||||
if checker.patch(Rule::UnusedVariable) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
pyflakes::fixes::remove_exception_handler_assignment(
|
||||
binding,
|
||||
checker.locator,
|
||||
)
|
||||
.map(Fix::automatic)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::InvalidAllFormat) {
|
||||
if let Some(diagnostic) = pylint::rules::invalid_all_format(binding) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::InvalidAllObject) {
|
||||
if let Some(diagnostic) = pylint::rules::invalid_all_object(binding) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::UnconventionalImportAlias) {
|
||||
if let Some(diagnostic) = flake8_import_conventions::rules::unconventional_import_alias(
|
||||
checker,
|
||||
binding,
|
||||
&checker.settings.flake8_import_conventions.aliases,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.is_stub {
|
||||
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding)
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
crates/ruff/src/checkers/ast/analyze/comprehension.rs
Normal file
16
crates/ruff/src/checkers/ast/analyze/comprehension.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use rustpython_parser::ast::Comprehension;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::flake8_simplify;
|
||||
|
||||
/// Run lint rules over a [`Comprehension`] syntax nodes.
|
||||
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_for(
|
||||
checker,
|
||||
&comprehension.target,
|
||||
&comprehension.iter,
|
||||
);
|
||||
}
|
||||
}
|
||||
32
crates/ruff/src/checkers/ast/analyze/deferred_for_loops.rs
Normal file
32
crates/ruff/src/checkers/ast/analyze/deferred_for_loops.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use rustpython_parser::ast::{self, Stmt};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bugbear, perflint};
|
||||
|
||||
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
|
||||
while !checker.deferred.for_loops.is_empty() {
|
||||
let for_loops = std::mem::take(&mut checker.deferred.for_loops);
|
||||
for snapshot in for_loops {
|
||||
checker.semantic.restore(snapshot);
|
||||
|
||||
if let Stmt::For(ast::StmtFor {
|
||||
target, iter, body, ..
|
||||
})
|
||||
| Stmt::AsyncFor(ast::StmtAsyncFor {
|
||||
target, iter, body, ..
|
||||
}) = &checker.semantic.stmt()
|
||||
{
|
||||
if checker.enabled(Rule::UnusedLoopControlVariable) {
|
||||
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
|
||||
}
|
||||
if checker.enabled(Rule::IncorrectDictIterator) {
|
||||
perflint::rules::incorrect_dict_iterator(checker, target, iter);
|
||||
}
|
||||
} else {
|
||||
unreachable!("Expected Expr::For | Expr::AsyncFor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs
Normal file
287
crates/ruff/src/checkers/ast/analyze/deferred_scopes.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::cast;
|
||||
use ruff_python_semantic::analyze::{branch_detection, visibility};
|
||||
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
|
||||
|
||||
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[
|
||||
Rule::GlobalVariableNotAssigned,
|
||||
Rule::ImportShadowedByLoopVar,
|
||||
Rule::RedefinedWhileUnused,
|
||||
Rule::RuntimeImportInTypeCheckingBlock,
|
||||
Rule::TypingOnlyFirstPartyImport,
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Rule::TypingOnlyThirdPartyImport,
|
||||
Rule::UndefinedLocal,
|
||||
Rule::UnusedAnnotation,
|
||||
Rule::UnusedClassMethodArgument,
|
||||
Rule::UnusedFunctionArgument,
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedLambdaArgument,
|
||||
Rule::UnusedMethodArgument,
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
Rule::UnusedVariable,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Identify any valid runtime imports. If a module is imported at runtime, and
|
||||
// used at runtime, then by default, we avoid flagging any other
|
||||
// imports from that model as typing-only.
|
||||
let enforce_typing_imports = !checker.is_stub
|
||||
&& checker.any_enabled(&[
|
||||
Rule::RuntimeImportInTypeCheckingBlock,
|
||||
Rule::TypingOnlyFirstPartyImport,
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Rule::TypingOnlyThirdPartyImport,
|
||||
]);
|
||||
let runtime_imports: Vec<Vec<&Binding>> = if enforce_typing_imports {
|
||||
checker
|
||||
.semantic
|
||||
.scopes
|
||||
.iter()
|
||||
.map(|scope| {
|
||||
scope
|
||||
.binding_ids()
|
||||
.map(|binding_id| checker.semantic.binding(binding_id))
|
||||
.filter(|binding| {
|
||||
flake8_type_checking::helpers::is_valid_runtime_import(
|
||||
binding,
|
||||
&checker.semantic,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
for scope_id in checker.deferred.scopes.iter().rev().copied() {
|
||||
let scope = &checker.semantic.scopes[scope_id];
|
||||
|
||||
if checker.enabled(Rule::UndefinedLocal) {
|
||||
pyflakes::rules::undefined_local(checker, scope_id, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::GlobalVariableNotAssigned) {
|
||||
for (name, binding_id) in scope.bindings() {
|
||||
let binding = checker.semantic.binding(binding_id);
|
||||
if binding.kind.is_global() {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
pylint::rules::GlobalVariableNotAssigned {
|
||||
name: (*name).to_string(),
|
||||
},
|
||||
binding.range,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::ImportShadowedByLoopVar) {
|
||||
for (name, binding_id) in scope.bindings() {
|
||||
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
|
||||
// If the shadowing binding isn't a loop variable, abort.
|
||||
let binding = &checker.semantic.bindings[shadow.binding_id()];
|
||||
if !binding.kind.is_loop_var() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the shadowed binding isn't an import, abort.
|
||||
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
|
||||
if !matches!(
|
||||
shadowed.kind,
|
||||
BindingKind::Import(..)
|
||||
| BindingKind::FromImport(..)
|
||||
| BindingKind::SubmoduleImport(..)
|
||||
| BindingKind::FutureImport
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
branch_detection::different_forks(left, right, &checker.semantic.stmts)
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::RedefinedWhileUnused) {
|
||||
for (name, binding_id) in scope.bindings() {
|
||||
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
|
||||
// If the shadowing binding is a loop variable, abort, to avoid overlap
|
||||
// with F402.
|
||||
let binding = &checker.semantic.bindings[shadow.binding_id()];
|
||||
if binding.kind.is_loop_var() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the shadowed binding is used, abort.
|
||||
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
|
||||
if shadowed.is_used() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the shadowing binding isn't considered a "redefinition" of the
|
||||
// shadowed binding, abort.
|
||||
if !binding.redefines(shadowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if shadow.same_scope() {
|
||||
// If the symbol is a dummy variable, abort, unless the shadowed
|
||||
// binding is an import.
|
||||
if !matches!(
|
||||
shadowed.kind,
|
||||
BindingKind::Import(..)
|
||||
| BindingKind::FromImport(..)
|
||||
| BindingKind::SubmoduleImport(..)
|
||||
| BindingKind::FutureImport
|
||||
) && checker.settings.dummy_variable_rgx.is_match(name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this is an overloaded function, abort.
|
||||
if shadowed.kind.is_function_definition()
|
||||
&& visibility::is_overload(
|
||||
cast::decorator_list(
|
||||
checker.semantic.stmts[shadowed.source.unwrap()],
|
||||
),
|
||||
&checker.semantic,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Only enforce cross-scope shadowing for imports.
|
||||
if !matches!(
|
||||
shadowed.kind,
|
||||
BindingKind::Import(..)
|
||||
| BindingKind::FromImport(..)
|
||||
| BindingKind::SubmoduleImport(..)
|
||||
| BindingKind::FutureImport
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
branch_detection::different_forks(left, right, &checker.semantic.stmts)
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
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,
|
||||
);
|
||||
if let Some(range) = binding.parent_range(&checker.semantic) {
|
||||
diagnostic.set_parent(range.start());
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
scope.kind,
|
||||
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
|
||||
) {
|
||||
if checker.enabled(Rule::UnusedVariable) {
|
||||
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnusedAnnotation) {
|
||||
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
|
||||
}
|
||||
|
||||
if !checker.is_stub {
|
||||
if checker.any_enabled(&[
|
||||
Rule::UnusedClassMethodArgument,
|
||||
Rule::UnusedFunctionArgument,
|
||||
Rule::UnusedLambdaArgument,
|
||||
Rule::UnusedMethodArgument,
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
]) {
|
||||
flake8_unused_arguments::rules::unused_arguments(
|
||||
checker,
|
||||
scope,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
scope.kind,
|
||||
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
|
||||
) {
|
||||
if enforce_typing_imports {
|
||||
let runtime_imports: Vec<&Binding> = checker
|
||||
.semantic
|
||||
.scopes
|
||||
.ancestor_ids(scope_id)
|
||||
.flat_map(|scope_id| runtime_imports[scope_id.as_usize()].iter())
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
if checker.enabled(Rule::RuntimeImportInTypeCheckingBlock) {
|
||||
flake8_type_checking::rules::runtime_import_in_type_checking_block(
|
||||
checker,
|
||||
scope,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
|
||||
if checker.any_enabled(&[
|
||||
Rule::TypingOnlyFirstPartyImport,
|
||||
Rule::TypingOnlyStandardLibraryImport,
|
||||
Rule::TypingOnlyThirdPartyImport,
|
||||
]) {
|
||||
flake8_type_checking::rules::typing_only_runtime_import(
|
||||
checker,
|
||||
scope,
|
||||
&runtime_imports,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::UnusedImport) {
|
||||
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.extend(diagnostics);
|
||||
}
|
||||
291
crates/ruff/src/checkers/ast/analyze/definitions.rs
Normal file
291
crates/ruff/src/checkers/ast/analyze/definitions.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use ruff_python_ast::str::raw_contents_range;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Ranged;
|
||||
|
||||
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::docstrings::Docstring;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle};
|
||||
use crate::{docstrings, warn_user};
|
||||
|
||||
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
|
||||
///
|
||||
/// This phase is expected to run after the AST has been traversed in its entirety; as such,
|
||||
/// it is expected that all [`Definition`] nodes have been visited by the time, and that this
|
||||
/// method will not recurse into any other nodes.
|
||||
pub(crate) fn definitions(checker: &mut Checker) {
|
||||
let enforce_annotations = checker.any_enabled(&[
|
||||
Rule::AnyType,
|
||||
Rule::MissingReturnTypeClassMethod,
|
||||
Rule::MissingReturnTypePrivateFunction,
|
||||
Rule::MissingReturnTypeSpecialMethod,
|
||||
Rule::MissingReturnTypeStaticMethod,
|
||||
Rule::MissingReturnTypeUndocumentedPublicFunction,
|
||||
Rule::MissingTypeArgs,
|
||||
Rule::MissingTypeCls,
|
||||
Rule::MissingTypeFunctionArgument,
|
||||
Rule::MissingTypeKwargs,
|
||||
Rule::MissingTypeSelf,
|
||||
]);
|
||||
let enforce_stubs = checker.is_stub
|
||||
&& checker.any_enabled(&[Rule::DocstringInStub, Rule::IterMethodReturnIterable]);
|
||||
let enforce_docstrings = checker.any_enabled(&[
|
||||
Rule::BlankLineAfterLastSection,
|
||||
Rule::BlankLineAfterSummary,
|
||||
Rule::BlankLineBeforeClass,
|
||||
Rule::BlankLinesBetweenHeaderAndContent,
|
||||
Rule::CapitalizeSectionName,
|
||||
Rule::DashedUnderlineAfterSection,
|
||||
Rule::DocstringStartsWithThis,
|
||||
Rule::EmptyDocstring,
|
||||
Rule::EmptyDocstringSection,
|
||||
Rule::EndsInPeriod,
|
||||
Rule::EndsInPunctuation,
|
||||
Rule::EscapeSequenceInDocstring,
|
||||
Rule::FirstLineCapitalized,
|
||||
Rule::FitsOnOneLine,
|
||||
Rule::IndentWithSpaces,
|
||||
Rule::MultiLineSummaryFirstLine,
|
||||
Rule::MultiLineSummarySecondLine,
|
||||
Rule::NewLineAfterLastParagraph,
|
||||
Rule::NewLineAfterSectionName,
|
||||
Rule::NoBlankLineAfterFunction,
|
||||
Rule::NoBlankLineAfterSection,
|
||||
Rule::NoBlankLineBeforeFunction,
|
||||
Rule::NoBlankLineBeforeSection,
|
||||
Rule::NoSignature,
|
||||
Rule::NonImperativeMood,
|
||||
Rule::OneBlankLineAfterClass,
|
||||
Rule::OneBlankLineBeforeClass,
|
||||
Rule::OverIndentation,
|
||||
Rule::OverloadWithDocstring,
|
||||
Rule::SectionNameEndsInColon,
|
||||
Rule::SectionNotOverIndented,
|
||||
Rule::SectionUnderlineAfterName,
|
||||
Rule::SectionUnderlineMatchesSectionLength,
|
||||
Rule::SectionUnderlineNotOverIndented,
|
||||
Rule::SurroundingWhitespace,
|
||||
Rule::TripleSingleQuotes,
|
||||
Rule::UnderIndentation,
|
||||
Rule::UndocumentedMagicMethod,
|
||||
Rule::UndocumentedParam,
|
||||
Rule::UndocumentedPublicClass,
|
||||
Rule::UndocumentedPublicFunction,
|
||||
Rule::UndocumentedPublicInit,
|
||||
Rule::UndocumentedPublicMethod,
|
||||
Rule::UndocumentedPublicModule,
|
||||
Rule::UndocumentedPublicNestedClass,
|
||||
Rule::UndocumentedPublicPackage,
|
||||
]);
|
||||
|
||||
if !enforce_annotations && !enforce_docstrings && !enforce_stubs {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute visibility of all definitions.
|
||||
let exports: Option<Vec<&str>> = {
|
||||
checker
|
||||
.semantic
|
||||
.global_scope()
|
||||
.get_all("__all__")
|
||||
.map(|binding_id| &checker.semantic.bindings[binding_id])
|
||||
.filter_map(|binding| match &binding.kind {
|
||||
BindingKind::Export(Export { names }) => Some(names.iter().copied()),
|
||||
_ => None,
|
||||
})
|
||||
.fold(None, |acc, names| {
|
||||
Some(acc.into_iter().flatten().chain(names).collect())
|
||||
})
|
||||
};
|
||||
|
||||
let definitions = std::mem::take(&mut checker.semantic.definitions);
|
||||
let mut overloaded_name: Option<String> = None;
|
||||
for ContextualizedDefinition {
|
||||
definition,
|
||||
visibility,
|
||||
} in definitions.resolve(exports.as_deref()).iter()
|
||||
{
|
||||
let docstring = docstrings::extraction::extract_docstring(definition);
|
||||
|
||||
// flake8-annotations
|
||||
if enforce_annotations {
|
||||
// TODO(charlie): This should be even stricter, in that an overload
|
||||
// implementation should come immediately after the overloaded
|
||||
// interfaces, without any AST nodes in between. Right now, we
|
||||
// only error when traversing definition boundaries (functions,
|
||||
// classes, etc.).
|
||||
if !overloaded_name.map_or(false, |overloaded_name| {
|
||||
flake8_annotations::helpers::is_overload_impl(
|
||||
definition,
|
||||
&overloaded_name,
|
||||
&checker.semantic,
|
||||
)
|
||||
}) {
|
||||
checker
|
||||
.diagnostics
|
||||
.extend(flake8_annotations::rules::definition(
|
||||
checker,
|
||||
definition,
|
||||
*visibility,
|
||||
));
|
||||
}
|
||||
overloaded_name =
|
||||
flake8_annotations::helpers::overloaded_name(definition, &checker.semantic);
|
||||
}
|
||||
|
||||
// flake8-pyi
|
||||
if enforce_stubs {
|
||||
if checker.enabled(Rule::DocstringInStub) {
|
||||
flake8_pyi::rules::docstring_in_stubs(checker, docstring);
|
||||
}
|
||||
if checker.enabled(Rule::IterMethodReturnIterable) {
|
||||
flake8_pyi::rules::iter_method_return_iterable(checker, definition);
|
||||
}
|
||||
}
|
||||
|
||||
// pydocstyle
|
||||
if enforce_docstrings {
|
||||
if pydocstyle::helpers::should_ignore_definition(
|
||||
definition,
|
||||
&checker.settings.pydocstyle.ignore_decorators,
|
||||
&checker.semantic,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract a `Docstring` from a `Definition`.
|
||||
let Some(expr) = docstring else {
|
||||
pydocstyle::rules::not_missing(checker, definition, *visibility);
|
||||
continue;
|
||||
};
|
||||
|
||||
let contents = checker.locator.slice(expr.range());
|
||||
|
||||
let indentation = checker.locator.slice(TextRange::new(
|
||||
checker.locator.line_start(expr.start()),
|
||||
expr.start(),
|
||||
));
|
||||
|
||||
if pydocstyle::helpers::should_ignore_docstring(contents) {
|
||||
#[allow(deprecated)]
|
||||
let location = checker.locator.compute_source_location(expr.start());
|
||||
warn_user!(
|
||||
"Docstring at {}:{}:{} contains implicit string concatenation; ignoring...",
|
||||
relativize_path(checker.path),
|
||||
location.row,
|
||||
location.column
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// SAFETY: Safe for docstrings that pass `should_ignore_docstring`.
|
||||
let body_range = raw_contents_range(contents).unwrap();
|
||||
let docstring = Docstring {
|
||||
definition,
|
||||
expr,
|
||||
contents,
|
||||
body_range,
|
||||
indentation,
|
||||
};
|
||||
|
||||
if !pydocstyle::rules::not_empty(checker, &docstring) {
|
||||
continue;
|
||||
}
|
||||
if checker.enabled(Rule::FitsOnOneLine) {
|
||||
pydocstyle::rules::one_liner(checker, &docstring);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::NoBlankLineAfterFunction,
|
||||
Rule::NoBlankLineBeforeFunction,
|
||||
]) {
|
||||
pydocstyle::rules::blank_before_after_function(checker, &docstring);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::BlankLineBeforeClass,
|
||||
Rule::OneBlankLineAfterClass,
|
||||
Rule::OneBlankLineBeforeClass,
|
||||
]) {
|
||||
pydocstyle::rules::blank_before_after_class(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::BlankLineAfterSummary) {
|
||||
pydocstyle::rules::blank_after_summary(checker, &docstring);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::IndentWithSpaces,
|
||||
Rule::OverIndentation,
|
||||
Rule::UnderIndentation,
|
||||
]) {
|
||||
pydocstyle::rules::indent(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::NewLineAfterLastParagraph) {
|
||||
pydocstyle::rules::newline_after_last_paragraph(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::SurroundingWhitespace) {
|
||||
pydocstyle::rules::no_surrounding_whitespace(checker, &docstring);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::MultiLineSummaryFirstLine,
|
||||
Rule::MultiLineSummarySecondLine,
|
||||
]) {
|
||||
pydocstyle::rules::multi_line_summary_start(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::TripleSingleQuotes) {
|
||||
pydocstyle::rules::triple_quotes(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::EscapeSequenceInDocstring) {
|
||||
pydocstyle::rules::backslashes(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::EndsInPeriod) {
|
||||
pydocstyle::rules::ends_with_period(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::NonImperativeMood) {
|
||||
pydocstyle::rules::non_imperative_mood(
|
||||
checker,
|
||||
&docstring,
|
||||
&checker.settings.pydocstyle.property_decorators,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::NoSignature) {
|
||||
pydocstyle::rules::no_signature(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::FirstLineCapitalized) {
|
||||
pydocstyle::rules::capitalized(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::DocstringStartsWithThis) {
|
||||
pydocstyle::rules::starts_with_this(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::EndsInPunctuation) {
|
||||
pydocstyle::rules::ends_with_punctuation(checker, &docstring);
|
||||
}
|
||||
if checker.enabled(Rule::OverloadWithDocstring) {
|
||||
pydocstyle::rules::if_needed(checker, &docstring);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::BlankLineAfterLastSection,
|
||||
Rule::BlankLinesBetweenHeaderAndContent,
|
||||
Rule::CapitalizeSectionName,
|
||||
Rule::DashedUnderlineAfterSection,
|
||||
Rule::EmptyDocstringSection,
|
||||
Rule::MultiLineSummaryFirstLine,
|
||||
Rule::NewLineAfterSectionName,
|
||||
Rule::NoBlankLineAfterSection,
|
||||
Rule::NoBlankLineBeforeSection,
|
||||
Rule::SectionNameEndsInColon,
|
||||
Rule::SectionNotOverIndented,
|
||||
Rule::SectionUnderlineAfterName,
|
||||
Rule::SectionUnderlineMatchesSectionLength,
|
||||
Rule::SectionUnderlineNotOverIndented,
|
||||
Rule::UndocumentedParam,
|
||||
]) {
|
||||
pydocstyle::rules::sections(
|
||||
checker,
|
||||
&docstring,
|
||||
checker.settings.pydocstyle.convention.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
crates/ruff/src/checkers/ast/analyze/except_handler.rs
Normal file
88
crates/ruff/src/checkers/ast/analyze/except_handler.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use rustpython_parser::ast::{self, ExceptHandler, Ranged};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
flake8_bandit, flake8_blind_except, flake8_bugbear, flake8_builtins, pycodestyle, pylint,
|
||||
tryceratops,
|
||||
};
|
||||
|
||||
/// Run lint rules over an [`ExceptHandler`] syntax node.
|
||||
pub(crate) fn except_handler(except_handler: &ExceptHandler, checker: &mut Checker) {
|
||||
match except_handler {
|
||||
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
||||
type_,
|
||||
name,
|
||||
body,
|
||||
range: _,
|
||||
}) => {
|
||||
if checker.enabled(Rule::BareExcept) {
|
||||
if let Some(diagnostic) = pycodestyle::rules::bare_except(
|
||||
type_.as_deref(),
|
||||
body,
|
||||
except_handler,
|
||||
checker.locator,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::RaiseWithoutFromInsideExcept) {
|
||||
flake8_bugbear::rules::raise_without_from_inside_except(
|
||||
checker,
|
||||
name.as_deref(),
|
||||
body,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::BlindExcept) {
|
||||
flake8_blind_except::rules::blind_except(
|
||||
checker,
|
||||
type_.as_deref(),
|
||||
name.as_deref(),
|
||||
body,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::TryExceptPass) {
|
||||
flake8_bandit::rules::try_except_pass(
|
||||
checker,
|
||||
except_handler,
|
||||
type_.as_deref(),
|
||||
body,
|
||||
checker.settings.flake8_bandit.check_typed_exception,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::TryExceptContinue) {
|
||||
flake8_bandit::rules::try_except_continue(
|
||||
checker,
|
||||
except_handler,
|
||||
type_.as_deref(),
|
||||
body,
|
||||
checker.settings.flake8_bandit.check_typed_exception,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::ExceptWithEmptyTuple) {
|
||||
flake8_bugbear::rules::except_with_empty_tuple(checker, except_handler);
|
||||
}
|
||||
if checker.enabled(Rule::ExceptWithNonExceptionClasses) {
|
||||
flake8_bugbear::rules::except_with_non_exception_classes(checker, except_handler);
|
||||
}
|
||||
if checker.enabled(Rule::ReraiseNoCause) {
|
||||
tryceratops::rules::reraise_no_cause(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::BinaryOpException) {
|
||||
pylint::rules::binary_op_exception(checker, except_handler);
|
||||
}
|
||||
if let Some(name) = name {
|
||||
if checker.enabled(Rule::AmbiguousVariableName) {
|
||||
if let Some(diagnostic) =
|
||||
pycodestyle::rules::ambiguous_variable_name(name.as_str(), name.range())
|
||||
{
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::BuiltinVariableShadowing) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1388
crates/ruff/src/checkers/ast/analyze/expression.rs
Normal file
1388
crates/ruff/src/checkers/ast/analyze/expression.rs
Normal file
File diff suppressed because it is too large
Load Diff
27
crates/ruff/src/checkers/ast/analyze/mod.rs
Normal file
27
crates/ruff/src/checkers/ast/analyze/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub(super) use argument::argument;
|
||||
pub(super) use arguments::arguments;
|
||||
pub(super) use bindings::bindings;
|
||||
pub(super) use comprehension::comprehension;
|
||||
pub(super) use deferred_for_loops::deferred_for_loops;
|
||||
pub(super) use deferred_scopes::deferred_scopes;
|
||||
pub(super) use definitions::definitions;
|
||||
pub(super) use except_handler::except_handler;
|
||||
pub(super) use expression::expression;
|
||||
pub(super) use module::module;
|
||||
pub(super) use statement::statement;
|
||||
pub(super) use suite::suite;
|
||||
pub(super) use unresolved_references::unresolved_references;
|
||||
|
||||
mod argument;
|
||||
mod arguments;
|
||||
mod bindings;
|
||||
mod comprehension;
|
||||
mod deferred_for_loops;
|
||||
mod deferred_scopes;
|
||||
mod definitions;
|
||||
mod except_handler;
|
||||
mod expression;
|
||||
mod module;
|
||||
mod statement;
|
||||
mod suite;
|
||||
mod unresolved_references;
|
||||
12
crates/ruff/src/checkers/ast/analyze/module.rs
Normal file
12
crates/ruff/src/checkers/ast/analyze/module.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustpython_parser::ast::Suite;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::flake8_bugbear;
|
||||
|
||||
/// Run lint rules over a module.
|
||||
pub(crate) fn module(suite: &Suite, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::FStringDocstring) {
|
||||
flake8_bugbear::rules::f_string_docstring(checker, suite);
|
||||
}
|
||||
}
|
||||
1424
crates/ruff/src/checkers/ast/analyze/statement.rs
Normal file
1424
crates/ruff/src/checkers/ast/analyze/statement.rs
Normal file
File diff suppressed because it is too large
Load Diff
12
crates/ruff/src/checkers/ast/analyze/suite.rs
Normal file
12
crates/ruff/src/checkers/ast/analyze/suite.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustpython_parser::ast::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::flake8_pie;
|
||||
|
||||
/// Run lint rules over a suite of [`Stmt`] syntax nodes.
|
||||
pub(crate) fn suite(suite: &[Stmt], checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryPass) {
|
||||
flake8_pie::rules::no_unnecessary_pass(checker, suite);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_semantic::Exceptions;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::pyflakes;
|
||||
|
||||
/// Run lint rules over all [`UnresolvedReference`] entities in the [`SemanticModel`].
|
||||
pub(crate) fn unresolved_references(checker: &mut Checker) {
|
||||
if !checker.any_enabled(&[Rule::UndefinedLocalWithImportStarUsage, Rule::UndefinedName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for reference in checker.semantic.unresolved_references() {
|
||||
if reference.is_wildcard_import() {
|
||||
if checker.enabled(Rule::UndefinedLocalWithImportStarUsage) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedLocalWithImportStarUsage {
|
||||
name: reference.name(checker.locator).to_string(),
|
||||
},
|
||||
reference.range(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if checker.enabled(Rule::UndefinedName) {
|
||||
// Avoid flagging if `NameError` is handled.
|
||||
if reference.exceptions().contains(Exceptions::NAME_ERROR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow __path__.
|
||||
if checker.path.ends_with("__init__.py") {
|
||||
if reference.name(checker.locator) == "__path__" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: reference.name(checker.locator).to_string(),
|
||||
},
|
||||
reference.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,4 @@ pub(crate) struct Deferred<'a> {
|
||||
pub(crate) functions: Vec<Snapshot>,
|
||||
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) for_loops: Vec<Snapshot>,
|
||||
pub(crate) assignments: Vec<Snapshot>,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,21 @@
|
||||
//! Lint rules based on checking physical lines.
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_trivia::UniversalNewlines;
|
||||
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
|
||||
use crate::rules::flake8_executable::rules::{
|
||||
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
|
||||
};
|
||||
use crate::rules::pycodestyle::rules::{
|
||||
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
|
||||
tab_indentation, trailing_whitespace,
|
||||
};
|
||||
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
|
||||
use crate::rules::pylint;
|
||||
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub(crate) fn check_physical_lines(
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
indexer: &Indexer,
|
||||
@@ -31,15 +23,7 @@ pub(crate) fn check_physical_lines(
|
||||
settings: &Settings,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
let mut has_any_shebang = false;
|
||||
|
||||
let enforce_blanket_noqa = settings.rules.enabled(Rule::BlanketNOQA);
|
||||
let enforce_shebang_not_executable = settings.rules.enabled(Rule::ShebangNotExecutable);
|
||||
let enforce_shebang_missing = settings.rules.enabled(Rule::ShebangMissingExecutableFile);
|
||||
let enforce_shebang_whitespace = settings.rules.enabled(Rule::ShebangLeadingWhitespace);
|
||||
let enforce_shebang_newline = settings.rules.enabled(Rule::ShebangNotFirstLine);
|
||||
let enforce_shebang_python = settings.rules.enabled(Rule::ShebangMissingPython);
|
||||
let enforce_blanket_type_ignore = settings.rules.enabled(Rule::BlanketTypeIgnore);
|
||||
let enforce_doc_line_too_long = settings.rules.enabled(Rule::DocLineTooLong);
|
||||
let enforce_line_too_long = settings.rules.enabled(Rule::LineTooLong);
|
||||
let enforce_no_newline_at_end_of_file = settings.rules.enabled(Rule::MissingNewlineAtEndOfFile);
|
||||
@@ -53,7 +37,6 @@ pub(crate) fn check_physical_lines(
|
||||
let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
|
||||
|
||||
let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration);
|
||||
let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
|
||||
|
||||
let mut commented_lines_iter = indexer.comment_ranges().iter().peekable();
|
||||
let mut doc_lines_iter = doc_lines.iter().peekable();
|
||||
@@ -72,51 +55,6 @@ pub(crate) fn check_physical_lines(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_blanket_type_ignore {
|
||||
blanket_type_ignore(&mut diagnostics, &line);
|
||||
}
|
||||
|
||||
if enforce_blanket_noqa {
|
||||
blanket_noqa(&mut diagnostics, &line);
|
||||
}
|
||||
|
||||
if enforce_shebang_missing
|
||||
|| enforce_shebang_not_executable
|
||||
|| enforce_shebang_whitespace
|
||||
|| enforce_shebang_newline
|
||||
|| enforce_shebang_python
|
||||
{
|
||||
if let Some(shebang) = ShebangDirective::try_extract(&line) {
|
||||
has_any_shebang = true;
|
||||
if enforce_shebang_not_executable {
|
||||
if let Some(diagnostic) =
|
||||
shebang_not_executable(path, line.range(), &shebang)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_whitespace {
|
||||
if let Some(diagnostic) =
|
||||
shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_newline {
|
||||
if let Some(diagnostic) =
|
||||
shebang_newline(line.range(), &shebang, index == 0)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_python {
|
||||
if let Some(diagnostic) = shebang_python(line.range(), &shebang) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while doc_lines_iter
|
||||
@@ -169,12 +107,6 @@ pub(crate) fn check_physical_lines(
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_shebang_missing && !has_any_shebang {
|
||||
if let Some(diagnostic) = shebang_missing(path) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_copyright_notice {
|
||||
if let Some(diagnostic) = missing_copyright_notice(locator, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
@@ -186,8 +118,6 @@ pub(crate) fn check_physical_lines(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::lexer::lex;
|
||||
use rustpython_parser::Mode;
|
||||
|
||||
@@ -209,7 +139,6 @@ mod tests {
|
||||
|
||||
let check_with_max_line_length = |line_length: LineLength| {
|
||||
check_physical_lines(
|
||||
Path::new("foo.py"),
|
||||
&locator,
|
||||
&stylist,
|
||||
&indexer,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Lint rules based on token traversal.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
@@ -11,83 +13,37 @@ use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::ruff::rules::Context;
|
||||
use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_fixme, flake8_implicit_str_concat, flake8_pyi, flake8_quotes,
|
||||
flake8_todos, pycodestyle, pylint, pyupgrade, ruff,
|
||||
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
|
||||
flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub(crate) fn check_tokens(
|
||||
tokens: &[LexResult],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
is_stub: bool,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
let enforce_ambiguous_unicode_character = settings.rules.any_enabled(&[
|
||||
if settings.rules.enabled(Rule::BlanketNOQA) {
|
||||
pygrep_hooks::rules::blanket_noqa(&mut diagnostics, indexer, locator);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::BlanketTypeIgnore) {
|
||||
pygrep_hooks::rules::blanket_type_ignore(&mut diagnostics, indexer, locator);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::AmbiguousUnicodeCharacterString,
|
||||
Rule::AmbiguousUnicodeCharacterDocstring,
|
||||
Rule::AmbiguousUnicodeCharacterComment,
|
||||
]);
|
||||
let enforce_invalid_string_character = settings.rules.any_enabled(&[
|
||||
Rule::InvalidCharacterBackspace,
|
||||
Rule::InvalidCharacterSub,
|
||||
Rule::InvalidCharacterEsc,
|
||||
Rule::InvalidCharacterNul,
|
||||
Rule::InvalidCharacterZeroWidthSpace,
|
||||
]);
|
||||
let enforce_quotes = settings.rules.any_enabled(&[
|
||||
Rule::BadQuotesInlineString,
|
||||
Rule::BadQuotesMultilineString,
|
||||
Rule::BadQuotesDocstring,
|
||||
Rule::AvoidableEscapedQuote,
|
||||
]);
|
||||
let enforce_commented_out_code = settings.rules.enabled(Rule::CommentedOutCode);
|
||||
let enforce_compound_statements = settings.rules.any_enabled(&[
|
||||
Rule::MultipleStatementsOnOneLineColon,
|
||||
Rule::MultipleStatementsOnOneLineSemicolon,
|
||||
Rule::UselessSemicolon,
|
||||
]);
|
||||
let enforce_invalid_escape_sequence = settings.rules.enabled(Rule::InvalidEscapeSequence);
|
||||
let enforce_implicit_string_concatenation = settings.rules.any_enabled(&[
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
]);
|
||||
|
||||
let enforce_trailing_comma = settings.rules.any_enabled(&[
|
||||
Rule::MissingTrailingComma,
|
||||
Rule::TrailingCommaOnBareTuple,
|
||||
Rule::ProhibitedTrailingComma,
|
||||
]);
|
||||
let enforce_extraneous_parenthesis = settings.rules.enabled(Rule::ExtraneousParentheses);
|
||||
let enforce_type_comment_in_stub = settings.rules.enabled(Rule::TypeCommentInStub);
|
||||
|
||||
// Combine flake8_todos and flake8_fixme so that we can reuse detected [`TodoDirective`]s.
|
||||
let enforce_todos = settings.rules.any_enabled(&[
|
||||
Rule::InvalidTodoTag,
|
||||
Rule::MissingTodoAuthor,
|
||||
Rule::MissingTodoLink,
|
||||
Rule::MissingTodoColon,
|
||||
Rule::MissingTodoDescription,
|
||||
Rule::InvalidTodoCapitalization,
|
||||
Rule::MissingSpaceAfterTodoColon,
|
||||
Rule::LineContainsFixme,
|
||||
Rule::LineContainsXxx,
|
||||
Rule::LineContainsTodo,
|
||||
Rule::LineContainsHack,
|
||||
]);
|
||||
|
||||
// RUF001, RUF002, RUF003
|
||||
if enforce_ambiguous_unicode_character {
|
||||
]) {
|
||||
let mut state_machine = StateMachine::default();
|
||||
for &(ref tok, range) in tokens.iter().flatten() {
|
||||
let is_docstring = if enforce_ambiguous_unicode_character {
|
||||
state_machine.consume(tok)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
||||
ruff::rules::ambiguous_unicode_character(
|
||||
&mut diagnostics,
|
||||
@@ -108,13 +64,11 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
// ERA001
|
||||
if enforce_commented_out_code {
|
||||
if settings.rules.enabled(Rule::CommentedOutCode) {
|
||||
eradicate::rules::commented_out_code(&mut diagnostics, locator, indexer, settings);
|
||||
}
|
||||
|
||||
// W605
|
||||
if enforce_invalid_escape_sequence {
|
||||
if settings.rules.enabled(Rule::InvalidEscapeSequence) {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if tok.is_string() {
|
||||
pycodestyle::rules::invalid_escape_sequence(
|
||||
@@ -126,8 +80,14 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
}
|
||||
}
|
||||
// PLE2510, PLE2512, PLE2513
|
||||
if enforce_invalid_string_character {
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::InvalidCharacterBackspace,
|
||||
Rule::InvalidCharacterSub,
|
||||
Rule::InvalidCharacterEsc,
|
||||
Rule::InvalidCharacterNul,
|
||||
Rule::InvalidCharacterZeroWidthSpace,
|
||||
]) {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if tok.is_string() {
|
||||
pylint::rules::invalid_string_characters(&mut diagnostics, *range, locator);
|
||||
@@ -135,8 +95,11 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
// E701, E702, E703
|
||||
if enforce_compound_statements {
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::MultipleStatementsOnOneLineColon,
|
||||
Rule::MultipleStatementsOnOneLineSemicolon,
|
||||
Rule::UselessSemicolon,
|
||||
]) {
|
||||
pycodestyle::rules::compound_statements(
|
||||
&mut diagnostics,
|
||||
tokens,
|
||||
@@ -146,13 +109,19 @@ pub(crate) fn check_tokens(
|
||||
);
|
||||
}
|
||||
|
||||
// Q001, Q002, Q003
|
||||
if enforce_quotes {
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::BadQuotesInlineString,
|
||||
Rule::BadQuotesMultilineString,
|
||||
Rule::BadQuotesDocstring,
|
||||
Rule::AvoidableEscapedQuote,
|
||||
]) {
|
||||
flake8_quotes::rules::from_tokens(&mut diagnostics, tokens, locator, settings);
|
||||
}
|
||||
|
||||
// ISC001, ISC002
|
||||
if enforce_implicit_string_concatenation {
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
]) {
|
||||
flake8_implicit_str_concat::rules::implicit(
|
||||
&mut diagnostics,
|
||||
tokens,
|
||||
@@ -161,24 +130,45 @@ pub(crate) fn check_tokens(
|
||||
);
|
||||
}
|
||||
|
||||
// COM812, COM818, COM819
|
||||
if enforce_trailing_comma {
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::MissingTrailingComma,
|
||||
Rule::TrailingCommaOnBareTuple,
|
||||
Rule::ProhibitedTrailingComma,
|
||||
]) {
|
||||
flake8_commas::rules::trailing_commas(&mut diagnostics, tokens, locator, settings);
|
||||
}
|
||||
|
||||
// UP034
|
||||
if enforce_extraneous_parenthesis {
|
||||
if settings.rules.enabled(Rule::ExtraneousParentheses) {
|
||||
pyupgrade::rules::extraneous_parentheses(&mut diagnostics, tokens, locator, settings);
|
||||
}
|
||||
|
||||
// PYI033
|
||||
if enforce_type_comment_in_stub && is_stub {
|
||||
if is_stub && settings.rules.enabled(Rule::TypeCommentInStub) {
|
||||
flake8_pyi::rules::type_comment_in_stub(&mut diagnostics, locator, indexer);
|
||||
}
|
||||
|
||||
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
|
||||
// T001, T002, T003, T004
|
||||
if enforce_todos {
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::ShebangNotExecutable,
|
||||
Rule::ShebangMissingExecutableFile,
|
||||
Rule::ShebangLeadingWhitespace,
|
||||
Rule::ShebangNotFirstLine,
|
||||
Rule::ShebangMissingPython,
|
||||
]) {
|
||||
flake8_executable::rules::from_tokens(tokens, path, locator, settings, &mut diagnostics);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::InvalidTodoTag,
|
||||
Rule::MissingTodoAuthor,
|
||||
Rule::MissingTodoLink,
|
||||
Rule::MissingTodoColon,
|
||||
Rule::MissingTodoDescription,
|
||||
Rule::InvalidTodoCapitalization,
|
||||
Rule::MissingSpaceAfterTodoColon,
|
||||
Rule::LineContainsFixme,
|
||||
Rule::LineContainsXxx,
|
||||
Rule::LineContainsTodo,
|
||||
Rule::LineContainsHack,
|
||||
]) {
|
||||
let todo_comments: Vec<TodoComment> = indexer
|
||||
.comment_ranges()
|
||||
.iter()
|
||||
@@ -188,9 +178,7 @@ pub(crate) fn check_tokens(
|
||||
TodoComment::from_comment(comment, *comment_range, i)
|
||||
})
|
||||
.collect();
|
||||
|
||||
flake8_todos::rules::todos(&mut diagnostics, &todo_comments, locator, indexer, settings);
|
||||
|
||||
flake8_fixme::rules::todos(&mut diagnostics, &todo_comments);
|
||||
}
|
||||
|
||||
|
||||
@@ -172,10 +172,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0131") => (RuleGroup::Unspecified, rules::pylint::rules::TypeBivariance),
|
||||
(Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch),
|
||||
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
|
||||
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
|
||||
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
|
||||
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
|
||||
(Pylint, "E0101") => (RuleGroup::Unspecified, rules::pylint::rules::ReturnInInit),
|
||||
(Pylint, "E0116") => (RuleGroup::Unspecified, rules::pylint::rules::ContinueInFinally),
|
||||
@@ -214,6 +214,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
|
||||
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
|
||||
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
|
||||
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
|
||||
(Pylint, "W0131") => (RuleGroup::Unspecified, rules::pylint::rules::NamedExprWithoutContext),
|
||||
(Pylint, "W0406") => (RuleGroup::Unspecified, rules::pylint::rules::ImportSelf),
|
||||
@@ -221,6 +222,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W0603") => (RuleGroup::Unspecified, rules::pylint::rules::GlobalStatement),
|
||||
(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, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
@@ -235,7 +237,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Builtins, "003") => (RuleGroup::Unspecified, rules::flake8_builtins::rules::BuiltinAttributeShadowing),
|
||||
|
||||
// flake8-bugbear
|
||||
(Flake8Bugbear, "002") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnaryPrefixIncrement),
|
||||
(Flake8Bugbear, "002") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnaryPrefixIncrementDecrement),
|
||||
(Flake8Bugbear, "003") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::AssignmentToOsEnviron),
|
||||
(Flake8Bugbear, "004") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnreliableCallableCheck),
|
||||
(Flake8Bugbear, "005") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::StripWithMultiCharacters),
|
||||
@@ -652,6 +654,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub),
|
||||
(Flake8Pyi, "054") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NumericLiteralTooLong),
|
||||
(Flake8Pyi, "053") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StringOrBytesTooLong),
|
||||
(Flake8Pyi, "056") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFixtureIncorrectParenthesesStyle),
|
||||
@@ -755,6 +758,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "203") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetatime),
|
||||
(Flake8UsePathlib, "204") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetmtime),
|
||||
(Flake8UsePathlib, "205") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetctime),
|
||||
(Flake8UsePathlib, "206") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsSepSplit),
|
||||
|
||||
// flake8-logging-format
|
||||
(Flake8LoggingFormat, "001") => (RuleGroup::Unspecified, rules::flake8_logging_format::violations::LoggingStringFormat),
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use ruff_python_trivia::{is_python_whitespace, Cursor};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_trivia::Cursor;
|
||||
|
||||
/// A shebang directive (e.g., `#!/usr/bin/env python3`).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ShebangDirective<'a> {
|
||||
/// The offset of the directive contents (e.g., `/usr/bin/env python3`) from the start of the
|
||||
/// line.
|
||||
pub(crate) offset: TextSize,
|
||||
/// The contents of the directive (e.g., `"/usr/bin/env python3"`).
|
||||
pub(crate) contents: &'a str,
|
||||
}
|
||||
pub(crate) struct ShebangDirective<'a>(&'a str);
|
||||
|
||||
impl<'a> ShebangDirective<'a> {
|
||||
/// Parse a shebang directive from a line, or return `None` if the line does not contain a
|
||||
@@ -17,9 +12,6 @@ impl<'a> ShebangDirective<'a> {
|
||||
pub(crate) fn try_extract(line: &'a str) -> Option<Self> {
|
||||
let mut cursor = Cursor::new(line);
|
||||
|
||||
// Trim whitespace.
|
||||
cursor.eat_while(is_python_whitespace);
|
||||
|
||||
// Trim the `#!` prefix.
|
||||
if !cursor.eat_char('#') {
|
||||
return None;
|
||||
@@ -28,10 +20,15 @@ impl<'a> ShebangDirective<'a> {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
offset: line.text_len() - cursor.text_len(),
|
||||
contents: cursor.chars().as_str(),
|
||||
})
|
||||
Some(Self(cursor.chars().as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ShebangDirective<'_> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +56,12 @@ mod tests {
|
||||
assert_debug_snapshot!(ShebangDirective::try_extract(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shebang_match_trailing_comment() {
|
||||
let source = "#!/usr/bin/env python # trailing comment";
|
||||
assert_debug_snapshot!(ShebangDirective::try_extract(source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shebang_leading_space() {
|
||||
let source = " #!/usr/bin/env python";
|
||||
|
||||
@@ -2,9 +2,4 @@
|
||||
source: crates/ruff/src/comments/shebang.rs
|
||||
expression: "ShebangDirective::try_extract(source)"
|
||||
---
|
||||
Some(
|
||||
ShebangDirective {
|
||||
offset: 4,
|
||||
contents: "/usr/bin/env python",
|
||||
},
|
||||
)
|
||||
None
|
||||
|
||||
@@ -3,8 +3,7 @@ source: crates/ruff/src/comments/shebang.rs
|
||||
expression: "ShebangDirective::try_extract(source)"
|
||||
---
|
||||
Some(
|
||||
ShebangDirective {
|
||||
offset: 2,
|
||||
contents: "/usr/bin/env python",
|
||||
},
|
||||
ShebangDirective(
|
||||
"/usr/bin/env python",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: crates/ruff/src/comments/shebang.rs
|
||||
expression: "ShebangDirective::try_extract(source)"
|
||||
---
|
||||
Some(
|
||||
ShebangDirective(
|
||||
"/usr/bin/env python # trailing comment",
|
||||
),
|
||||
)
|
||||
@@ -71,12 +71,12 @@ pub fn extract_directives(
|
||||
indexer: &Indexer,
|
||||
) -> Directives {
|
||||
Directives {
|
||||
noqa_line_for: if flags.contains(Flags::NOQA) {
|
||||
noqa_line_for: if flags.intersects(Flags::NOQA) {
|
||||
extract_noqa_line_for(lxr, locator, indexer)
|
||||
} else {
|
||||
NoqaMapping::default()
|
||||
},
|
||||
isort: if flags.contains(Flags::ISORT) {
|
||||
isort: if flags.intersects(Flags::ISORT) {
|
||||
extract_isort_directives(lxr, locator)
|
||||
} else {
|
||||
IsortDirectives::default()
|
||||
|
||||
@@ -100,7 +100,9 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_tokens())
|
||||
{
|
||||
let is_stub = is_python_stub_file(path);
|
||||
diagnostics.extend(check_tokens(locator, indexer, &tokens, settings, is_stub));
|
||||
diagnostics.extend(check_tokens(
|
||||
&tokens, path, locator, indexer, settings, is_stub,
|
||||
));
|
||||
}
|
||||
|
||||
// Run the filesystem-based rules.
|
||||
@@ -193,7 +195,7 @@ pub fn check_path(
|
||||
.any(|rule_code| rule_code.lint_source().is_physical_lines())
|
||||
{
|
||||
diagnostics.extend(check_physical_lines(
|
||||
path, locator, stylist, indexer, &doc_lines, settings,
|
||||
locator, stylist, indexer, &doc_lines, settings,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -109,11 +109,11 @@ impl Emitter for TextEmitter {
|
||||
sep = ":".cyan(),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message,
|
||||
show_fix_status: self.flags.contains(EmitterFlags::SHOW_FIX_STATUS)
|
||||
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS)
|
||||
}
|
||||
)?;
|
||||
|
||||
if self.flags.contains(EmitterFlags::SHOW_SOURCE) {
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
@@ -124,7 +124,7 @@ impl Emitter for TextEmitter {
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.flags.contains(EmitterFlags::SHOW_FIX_DIFF) {
|
||||
if self.flags.intersects(EmitterFlags::SHOW_FIX_DIFF) {
|
||||
if let Some(diff) = Diff::from_message(message) {
|
||||
writeln!(writer, "{diff}")?;
|
||||
}
|
||||
|
||||
@@ -68,12 +68,8 @@ impl<'a> Directive<'a> {
|
||||
|
||||
// If the next character is `:`, then it's a list of codes. Otherwise, it's a directive
|
||||
// to ignore all rules.
|
||||
return Ok(Some(
|
||||
if text[noqa_literal_end..]
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| c == ':')
|
||||
{
|
||||
let directive = match text[noqa_literal_end..].chars().next() {
|
||||
Some(':') => {
|
||||
// E.g., `# noqa: F401, F841`.
|
||||
let mut codes_start = noqa_literal_end;
|
||||
|
||||
@@ -120,8 +116,9 @@ impl<'a> Directive<'a> {
|
||||
range: range.add(offset),
|
||||
codes,
|
||||
})
|
||||
} else {
|
||||
// E.g., `# noqa`.
|
||||
}
|
||||
None | Some('#') => {
|
||||
// E.g., `# noqa` or `# noqa# ignore`.
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(comment_start).unwrap(),
|
||||
TextSize::try_from(noqa_literal_end).unwrap(),
|
||||
@@ -129,8 +126,21 @@ impl<'a> Directive<'a> {
|
||||
Self::All(All {
|
||||
range: range.add(offset),
|
||||
})
|
||||
},
|
||||
));
|
||||
}
|
||||
Some(c) if c.is_whitespace() => {
|
||||
// E.g., `# noqa # ignore`.
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(comment_start).unwrap(),
|
||||
TextSize::try_from(noqa_literal_end).unwrap(),
|
||||
);
|
||||
Self::All(All {
|
||||
range: range.add(offset),
|
||||
})
|
||||
}
|
||||
_ => return Err(ParseError::InvalidSuffix),
|
||||
};
|
||||
|
||||
return Ok(Some(directive));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -237,7 +247,7 @@ impl FileExemption {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
let path_display = relativize_path(path);
|
||||
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
|
||||
warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}");
|
||||
}
|
||||
Ok(Some(ParsedFileExemption::All)) => {
|
||||
return Some(Self::All);
|
||||
@@ -250,7 +260,8 @@ impl FileExemption {
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
warn!("Invalid code provided to `# ruff: noqa` on line {line}: {code}");
|
||||
let path_display = relativize_path(path);
|
||||
warn!("Invalid code provided to `# ruff: noqa` at {path_display}:{line}: {code}");
|
||||
None
|
||||
}
|
||||
}));
|
||||
@@ -904,6 +915,12 @@ mod tests {
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_invalid_suffix() {
|
||||
let source = "# noqa[F401]";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flake8_exemption_all() {
|
||||
let source = "# flake8: noqa";
|
||||
|
||||
@@ -238,23 +238,16 @@ impl Rule {
|
||||
match self {
|
||||
Rule::InvalidPyprojectToml => LintSource::PyprojectToml,
|
||||
Rule::UnusedNOQA => LintSource::Noqa,
|
||||
Rule::BlanketNOQA
|
||||
| Rule::BlanketTypeIgnore
|
||||
Rule::BidirectionalUnicode
|
||||
| Rule::BlankLineWithWhitespace
|
||||
| Rule::DocLineTooLong
|
||||
| Rule::LineTooLong
|
||||
| Rule::MixedSpacesAndTabs
|
||||
| Rule::MissingNewlineAtEndOfFile
|
||||
| Rule::UTF8EncodingDeclaration
|
||||
| Rule::ShebangMissingExecutableFile
|
||||
| Rule::ShebangNotExecutable
|
||||
| Rule::ShebangNotFirstLine
|
||||
| Rule::BidirectionalUnicode
|
||||
| Rule::ShebangMissingPython
|
||||
| Rule::ShebangLeadingWhitespace
|
||||
| Rule::TrailingWhitespace
|
||||
| Rule::TabIndentation
|
||||
| Rule::MissingCopyrightNotice
|
||||
| Rule::BlankLineWithWhitespace => LintSource::PhysicalLines,
|
||||
| Rule::MissingNewlineAtEndOfFile
|
||||
| Rule::MixedSpacesAndTabs
|
||||
| Rule::TabIndentation
|
||||
| Rule::TrailingWhitespace
|
||||
| Rule::UTF8EncodingDeclaration => LintSource::PhysicalLines,
|
||||
Rule::AmbiguousUnicodeCharacterComment
|
||||
| Rule::AmbiguousUnicodeCharacterDocstring
|
||||
| Rule::AmbiguousUnicodeCharacterString
|
||||
@@ -262,34 +255,41 @@ impl Rule {
|
||||
| Rule::BadQuotesDocstring
|
||||
| Rule::BadQuotesInlineString
|
||||
| Rule::BadQuotesMultilineString
|
||||
| Rule::BlanketNOQA
|
||||
| Rule::BlanketTypeIgnore
|
||||
| Rule::CommentedOutCode
|
||||
| Rule::MultiLineImplicitStringConcatenation
|
||||
| Rule::ExtraneousParentheses
|
||||
| Rule::InvalidCharacterBackspace
|
||||
| Rule::InvalidCharacterSub
|
||||
| Rule::InvalidCharacterEsc
|
||||
| Rule::InvalidCharacterNul
|
||||
| Rule::InvalidCharacterSub
|
||||
| Rule::InvalidCharacterZeroWidthSpace
|
||||
| Rule::ExtraneousParentheses
|
||||
| Rule::InvalidEscapeSequence
|
||||
| Rule::SingleLineImplicitStringConcatenation
|
||||
| Rule::MissingTrailingComma
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::MultipleStatementsOnOneLineColon
|
||||
| Rule::UselessSemicolon
|
||||
| Rule::MultipleStatementsOnOneLineSemicolon
|
||||
| Rule::ProhibitedTrailingComma
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::InvalidTodoTag
|
||||
| Rule::MissingTodoAuthor
|
||||
| Rule::MissingTodoLink
|
||||
| Rule::MissingTodoColon
|
||||
| Rule::MissingTodoDescription
|
||||
| Rule::InvalidTodoCapitalization
|
||||
| Rule::MissingSpaceAfterTodoColon
|
||||
| Rule::InvalidTodoTag
|
||||
| Rule::LineContainsFixme
|
||||
| Rule::LineContainsHack
|
||||
| Rule::LineContainsTodo
|
||||
| Rule::LineContainsXxx => LintSource::Tokens,
|
||||
| Rule::LineContainsXxx
|
||||
| Rule::MissingSpaceAfterTodoColon
|
||||
| Rule::MissingTodoAuthor
|
||||
| Rule::MissingTodoColon
|
||||
| Rule::MissingTodoDescription
|
||||
| Rule::MissingTodoLink
|
||||
| Rule::MissingTrailingComma
|
||||
| Rule::MultiLineImplicitStringConcatenation
|
||||
| Rule::MultipleStatementsOnOneLineColon
|
||||
| Rule::MultipleStatementsOnOneLineSemicolon
|
||||
| Rule::ProhibitedTrailingComma
|
||||
| Rule::ShebangLeadingWhitespace
|
||||
| Rule::ShebangMissingExecutableFile
|
||||
| Rule::ShebangMissingPython
|
||||
| Rule::ShebangNotExecutable
|
||||
| Rule::ShebangNotFirstLine
|
||||
| Rule::SingleLineImplicitStringConcatenation
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::UselessSemicolon => LintSource::Tokens,
|
||||
Rule::IOError => LintSource::Io,
|
||||
Rule::UnsortedImports | Rule::MissingRequiredImport => LintSource::Imports,
|
||||
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => LintSource::Filesystem,
|
||||
|
||||
@@ -431,18 +431,22 @@ fn is_file_excluded(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{create_dir, File};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::GlobSet;
|
||||
use itertools::Itertools;
|
||||
use path_absolutize::Absolutize;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::resolver::{
|
||||
is_file_excluded, match_exclusion, resolve_settings_with_processor, NoOpProcessor,
|
||||
PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
|
||||
is_file_excluded, match_exclusion, python_files_in_path, resolve_settings_with_processor,
|
||||
NoOpProcessor, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, Resolver,
|
||||
};
|
||||
use crate::settings::pyproject::find_settings_toml;
|
||||
use crate::settings::types::FilePattern;
|
||||
use crate::settings::AllSettings;
|
||||
use crate::test::test_resource_path;
|
||||
|
||||
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
|
||||
@@ -606,4 +610,43 @@ mod tests {
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_python_files() -> Result<()> {
|
||||
// Initialize the filesystem:
|
||||
// root
|
||||
// ├── file1.py
|
||||
// ├── dir1.py
|
||||
// │ └── file2.py
|
||||
// └── dir2.py
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let root = tmp_dir.path();
|
||||
let file1 = root.join("file1.py");
|
||||
let dir1 = root.join("dir1.py");
|
||||
let file2 = dir1.join("file2.py");
|
||||
let dir2 = root.join("dir2.py");
|
||||
File::create(&file1)?;
|
||||
create_dir(dir1)?;
|
||||
File::create(&file2)?;
|
||||
create_dir(dir2)?;
|
||||
|
||||
let (paths, _) = python_files_in_path(
|
||||
&[root.to_path_buf()],
|
||||
&PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Fixed,
|
||||
AllSettings::default(),
|
||||
None,
|
||||
),
|
||||
&NoOpProcessor,
|
||||
)?;
|
||||
let paths = paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ignore::DirEntry::path)
|
||||
.sorted()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(paths, &[file2, file1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
use rustpython_parser::ast::Constant;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -73,9 +74,7 @@ pub(crate) fn variable_name_task_id(
|
||||
}
|
||||
|
||||
// If the call doesn't have a `task_id` keyword argument, we can't do anything.
|
||||
let keyword = keywords
|
||||
.iter()
|
||||
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "task_id"))?;
|
||||
let keyword = find_keyword(keywords, "task_id")?;
|
||||
|
||||
// If the keyword argument is not a string, we can't do anything.
|
||||
let task_id = match &keyword.value {
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(super) fn match_function_def(
|
||||
body,
|
||||
decorator_list,
|
||||
),
|
||||
_ => panic!("Found non-FunctionDef in match_name"),
|
||||
_ => panic!("Found non-FunctionDef in match_function_def"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that an expression is annotated with a more specific type than
|
||||
/// Checks that function arguments are annotated with a more specific type than
|
||||
/// `Any`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
@@ -399,9 +399,23 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// Type aliases are unsupported and can lead to false positives.
|
||||
/// For example, the following will trigger this rule inadvertently:
|
||||
/// ```python
|
||||
/// from typing import Any
|
||||
///
|
||||
/// MyAny = Any
|
||||
///
|
||||
///
|
||||
/// def foo(x: MyAny):
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
|
||||
/// - [`typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
|
||||
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
|
||||
/// - [Mypy: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
|
||||
#[violation]
|
||||
pub struct AnyType {
|
||||
@@ -448,11 +462,12 @@ fn check_dynamically_typed<F>(
|
||||
}) = annotation
|
||||
{
|
||||
// Quoted annotations
|
||||
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) {
|
||||
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator())
|
||||
{
|
||||
if type_hint_resolves_to_any(
|
||||
&parsed_annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.locator(),
|
||||
checker.settings.target_version.minor(),
|
||||
) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
@@ -465,7 +480,7 @@ fn check_dynamically_typed<F>(
|
||||
if type_hint_resolves_to_any(
|
||||
annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.locator(),
|
||||
checker.settings.target_version.minor(),
|
||||
) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
@@ -690,7 +705,7 @@ pub(crate) fn definition(
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::add_return_annotation(checker.locator, stmt, "None")
|
||||
fixes::add_return_annotation(checker.locator(), stmt, "None")
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
@@ -708,7 +723,7 @@ pub(crate) fn definition(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(return_type) = simple_magic_return_type(name) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::add_return_annotation(checker.locator, stmt, return_type)
|
||||
fixes::add_return_annotation(checker.locator(), stmt, return_type)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Operator, Ranged};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::helpers::SimpleCallArgs;
|
||||
use ruff_python_ast::helpers::CallArguments;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn bad_file_permissions(
|
||||
matches!(call_path.as_slice(), ["os", "chmod"])
|
||||
})
|
||||
{
|
||||
let call_args = SimpleCallArgs::new(args, keywords);
|
||||
let call_args = CallArguments::new(args, keywords);
|
||||
if let Some(mode_arg) = call_args.argument("mode", 1) {
|
||||
if let Some(int_value) = int_value(mode_arg, checker.semantic()) {
|
||||
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{is_const_false, SimpleCallArgs};
|
||||
use ruff_python_ast::helpers::{find_keyword, is_const_false, CallArguments};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -78,15 +78,12 @@ pub(crate) fn hashlib_insecure_hash_functions(
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
if !is_used_for_security(keywords) {
|
||||
return;
|
||||
}
|
||||
match hashlib_call {
|
||||
HashlibCall::New => {
|
||||
let call_args = SimpleCallArgs::new(args, keywords);
|
||||
|
||||
if !is_used_for_security(&call_args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(name_arg) = call_args.argument("name", 0) {
|
||||
if let Some(name_arg) = CallArguments::new(args, keywords).argument("name", 0) {
|
||||
if let Some(hash_func_name) = string_literal(name_arg) {
|
||||
// `hashlib.new` accepts both lowercase and uppercase names for hash
|
||||
// functions.
|
||||
@@ -105,12 +102,6 @@ pub(crate) fn hashlib_insecure_hash_functions(
|
||||
}
|
||||
}
|
||||
HashlibCall::WeakHash(func_name) => {
|
||||
let call_args = SimpleCallArgs::new(args, keywords);
|
||||
|
||||
if !is_used_for_security(&call_args) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
HashlibInsecureHashFunction {
|
||||
string: (*func_name).to_string(),
|
||||
@@ -122,13 +113,12 @@ pub(crate) fn hashlib_insecure_hash_functions(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_used_for_security(call_args: &SimpleCallArgs) -> bool {
|
||||
match call_args.keyword_argument("usedforsecurity") {
|
||||
Some(expr) => !is_const_false(expr),
|
||||
_ => true,
|
||||
}
|
||||
fn is_used_for_security(keywords: &[Keyword]) -> bool {
|
||||
find_keyword(keywords, "usedforsecurity")
|
||||
.map_or(true, |keyword| !is_const_false(&keyword.value))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HashlibCall {
|
||||
New,
|
||||
WeakHash(&'static str),
|
||||
|
||||
@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -28,12 +29,7 @@ pub(crate) fn logging_config_insecure_listen(
|
||||
matches!(call_path.as_slice(), ["logging", "config", "listen"])
|
||||
})
|
||||
{
|
||||
if keywords.iter().any(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "verify")
|
||||
}) {
|
||||
if find_keyword(keywords, "verify").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::Truthiness;
|
||||
use ruff_python_ast::helpers::{find_keyword, Truthiness};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::{
|
||||
@@ -272,13 +272,10 @@ fn find_shell_keyword<'a>(
|
||||
keywords: &'a [Keyword],
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<ShellKeyword<'a>> {
|
||||
keywords
|
||||
.iter()
|
||||
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "shell"))
|
||||
.map(|keyword| ShellKeyword {
|
||||
truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)),
|
||||
keyword,
|
||||
})
|
||||
find_keyword(keywords, "shell").map(|keyword| ShellKeyword {
|
||||
truthiness: Truthiness::from_expr(&keyword.value, |id| semantic.is_builtin(id)),
|
||||
keyword,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the value provided to the `shell` call seems safe. This is based on Bandit's
|
||||
|
||||
@@ -3,6 +3,7 @@ use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -50,12 +51,7 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, func: &Expr, keywords
|
||||
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "CommunityData"])
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = keywords.iter().find(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "mpModel")
|
||||
}) {
|
||||
if let Some(keyword) = find_keyword(keywords, "mpModel") {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(value),
|
||||
..
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::SimpleCallArgs;
|
||||
use ruff_python_ast::helpers::CallArguments;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -36,8 +36,7 @@ impl Violation for SnmpWeakCryptography {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is \
|
||||
insecure."
|
||||
"You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -56,8 +55,7 @@ pub(crate) fn snmp_weak_cryptography(
|
||||
matches!(call_path.as_slice(), ["pysnmp", "hlapi", "UsmUserData"])
|
||||
})
|
||||
{
|
||||
let call_args = SimpleCallArgs::new(args, keywords);
|
||||
if call_args.len() < 3 {
|
||||
if CallArguments::new(args, keywords).len() < 3 {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(SnmpWeakCryptography, func.range()));
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::SimpleCallArgs;
|
||||
use ruff_python_ast::helpers::CallArguments;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn unsafe_yaml_load(
|
||||
matches!(call_path.as_slice(), ["yaml", "load"])
|
||||
})
|
||||
{
|
||||
let call_args = SimpleCallArgs::new(args, keywords);
|
||||
let call_args = CallArguments::new(args, keywords);
|
||||
if let Some(loader_arg) = call_args.argument("Loader", 1) {
|
||||
if !checker
|
||||
.semantic()
|
||||
|
||||
@@ -95,7 +95,11 @@ pub(crate) fn blind_except(
|
||||
if body.iter().any(|stmt| {
|
||||
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
|
||||
if let Expr::Call(ast::ExprCall { func, keywords, .. }) = value.as_ref() {
|
||||
if logging::is_logger_candidate(func, checker.semantic()) {
|
||||
if logging::is_logger_candidate(
|
||||
func,
|
||||
checker.semantic(),
|
||||
&checker.settings.logger_objects,
|
||||
) {
|
||||
if let Some(attribute) = func.as_attribute_expr() {
|
||||
let attr = attribute.attr.as_str();
|
||||
if attr == "exception" {
|
||||
|
||||
@@ -42,7 +42,7 @@ mod tests {
|
||||
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))]
|
||||
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))]
|
||||
#[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))]
|
||||
#[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))]
|
||||
#[test_case(Rule::UnaryPrefixIncrementDecrement, Path::new("B002.py"))]
|
||||
#[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))]
|
||||
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))]
|
||||
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))]
|
||||
|
||||
@@ -4,6 +4,7 @@ use rustpython_parser::ast::{self, Expr, Ranged, WithItem};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -115,9 +116,7 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem])
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["pytest", "raises"])
|
||||
})
|
||||
&& !keywords
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "match"))
|
||||
&& find_keyword(keywords, "match").is_none()
|
||||
{
|
||||
AssertionKind::PytestRaises
|
||||
} else {
|
||||
|
||||
@@ -276,7 +276,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
|
||||
}
|
||||
|
||||
/// B023
|
||||
pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &Node<'a>) {
|
||||
pub(crate) fn function_uses_loop_variable(checker: &mut Checker, node: &Node) {
|
||||
// Identify any "suspicious" variables. These are defined as variables that are
|
||||
// referenced in a function or lambda body, but aren't bound as arguments.
|
||||
let suspicious_variables = {
|
||||
@@ -303,8 +303,8 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: &
|
||||
// loop, flag it.
|
||||
for name in suspicious_variables {
|
||||
if reassigned_in_loop.contains(&name.id.as_str()) {
|
||||
if !checker.flake8_bugbear_seen.contains(&name) {
|
||||
checker.flake8_bugbear_seen.push(name);
|
||||
if !checker.flake8_bugbear_seen.contains(&name.range()) {
|
||||
checker.flake8_bugbear_seen.push(name.range());
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
FunctionUsesLoopVariable {
|
||||
name: name.id.to_string(),
|
||||
|
||||
@@ -80,7 +80,7 @@ pub(crate) fn getattr_with_constant(
|
||||
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
format!("{}.{}", checker.locator.slice(obj.range()), value),
|
||||
format!("{}.{}", checker.locator().slice(obj.range()), value),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(crate) use reuse_of_groupby_generator::*;
|
||||
pub(crate) use setattr_with_constant::*;
|
||||
pub(crate) use star_arg_unpacking_after_keyword_arg::*;
|
||||
pub(crate) use strip_with_multi_characters::*;
|
||||
pub(crate) use unary_prefix_increment::*;
|
||||
pub(crate) use unary_prefix_increment_decrement::*;
|
||||
pub(crate) use unintentional_type_annotation::*;
|
||||
pub(crate) use unreliable_callable_check::*;
|
||||
pub(crate) use unused_loop_control_variable::*;
|
||||
@@ -57,7 +57,7 @@ mod reuse_of_groupby_generator;
|
||||
mod setattr_with_constant;
|
||||
mod star_arg_unpacking_after_keyword_arg;
|
||||
mod strip_with_multi_characters;
|
||||
mod unary_prefix_increment;
|
||||
mod unary_prefix_increment_decrement;
|
||||
mod unintentional_type_annotation;
|
||||
mod unreliable_callable_check;
|
||||
mod unused_loop_control_variable;
|
||||
|
||||
@@ -2,6 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -48,12 +49,7 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, func: &Expr, keyword
|
||||
return;
|
||||
}
|
||||
|
||||
if keywords.iter().any(|keyword| {
|
||||
keyword
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg.as_str() == "stacklevel")
|
||||
}) {
|
||||
if find_keyword(keywords, "stacklevel").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of the unary prefix increment operator (e.g., `++n`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python does not support the unary prefix increment operator. Writing `++n`
|
||||
/// is equivalent to `+(+(n))`, which is equivalent to `n`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ++n
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// n += 1
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)
|
||||
/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)
|
||||
#[violation]
|
||||
pub struct UnaryPrefixIncrement;
|
||||
|
||||
impl Violation for UnaryPrefixIncrement {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Python does not support the unary prefix increment")
|
||||
}
|
||||
}
|
||||
|
||||
/// B002
|
||||
pub(crate) fn unary_prefix_increment(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
op: UnaryOp,
|
||||
operand: &Expr,
|
||||
) {
|
||||
if !matches!(op, UnaryOp::UAdd) {
|
||||
return;
|
||||
}
|
||||
let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else {
|
||||
return;
|
||||
};
|
||||
if !matches!(op, UnaryOp::UAdd) {
|
||||
return;
|
||||
}
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UnaryPrefixIncrement, expr.range()));
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the attempted use of the unary prefix increment (`++`) or
|
||||
/// decrement operator (`--`).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Python does not support the unary prefix increment or decrement operator.
|
||||
/// Writing `++n` is equivalent to `+(+(n))` and writing `--n` is equivalent to
|
||||
/// `-(-(n))`. In both cases, it is equivalent to `n`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// ++x
|
||||
/// --y
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// x += 1
|
||||
/// y -= 1
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)
|
||||
/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)
|
||||
#[violation]
|
||||
pub struct UnaryPrefixIncrementDecrement {
|
||||
operator: UnaryPrefixOperatorType,
|
||||
}
|
||||
|
||||
impl Violation for UnaryPrefixIncrementDecrement {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnaryPrefixIncrementDecrement { operator } = self;
|
||||
match operator {
|
||||
UnaryPrefixOperatorType::Increment => {
|
||||
format!("Python does not support the unary prefix increment operator (`++`)")
|
||||
}
|
||||
UnaryPrefixOperatorType::Decrement => {
|
||||
format!("Python does not support the unary prefix decrement operator (`--`)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// B002
|
||||
pub(crate) fn unary_prefix_increment_decrement(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
op: UnaryOp,
|
||||
operand: &Expr,
|
||||
) {
|
||||
let Expr::UnaryOp(ast::ExprUnaryOp { op: nested_op, .. }) = operand else {
|
||||
return;
|
||||
};
|
||||
match (op, nested_op) {
|
||||
(UnaryOp::UAdd, UnaryOp::UAdd) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UnaryPrefixIncrementDecrement {
|
||||
operator: UnaryPrefixOperatorType::Increment,
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
(UnaryOp::USub, UnaryOp::USub) => {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
UnaryPrefixIncrementDecrement {
|
||||
operator: UnaryPrefixOperatorType::Decrement,
|
||||
},
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum UnaryPrefixOperatorType {
|
||||
Increment,
|
||||
Decrement,
|
||||
}
|
||||
@@ -83,7 +83,7 @@ pub(crate) fn unreliable_callable_check(
|
||||
if id == "hasattr" {
|
||||
if checker.semantic().is_builtin("callable") {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("callable({})", checker.locator.slice(obj.range())),
|
||||
format!("callable({})", checker.locator().slice(obj.range())),
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_none;
|
||||
use ruff_python_ast::helpers::{find_keyword, is_const_none};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -51,9 +51,7 @@ pub(crate) fn zip_without_explicit_strict(
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = func {
|
||||
if id == "zip"
|
||||
&& checker.semantic().is_builtin("zip")
|
||||
&& !kwargs
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.as_ref().map_or(false, |name| name == "strict"))
|
||||
&& find_keyword(kwargs, "strict").is_none()
|
||||
&& !args
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B002.py:15:9: B002 Python does not support the unary prefix increment
|
||||
B002.py:18:9: B002 Python does not support the unary prefix increment operator (`++`)
|
||||
|
|
||||
14 | def this_is_buggy(n):
|
||||
15 | x = ++n
|
||||
17 | def this_is_buggy(n):
|
||||
18 | x = ++n
|
||||
| ^^^ B002
|
||||
16 | return x
|
||||
19 | y = --n
|
||||
20 | return x, y
|
||||
|
|
||||
|
||||
B002.py:20:12: B002 Python does not support the unary prefix increment
|
||||
B002.py:19:9: B002 Python does not support the unary prefix decrement operator (`--`)
|
||||
|
|
||||
19 | def this_is_buggy_too(n):
|
||||
20 | return ++n
|
||||
17 | def this_is_buggy(n):
|
||||
18 | x = ++n
|
||||
19 | y = --n
|
||||
| ^^^ B002
|
||||
20 | return x, y
|
||||
|
|
||||
|
||||
B002.py:24:12: B002 Python does not support the unary prefix increment operator (`++`)
|
||||
|
|
||||
23 | def this_is_buggy_too(n):
|
||||
24 | return ++n, --n
|
||||
| ^^^ B002
|
||||
|
|
||||
|
||||
B002.py:24:17: B002 Python does not support the unary prefix decrement operator (`--`)
|
||||
|
|
||||
23 | def this_is_buggy_too(n):
|
||||
24 | return ++n, --n
|
||||
| ^^^ B002
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::Decorator;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any class attributes that use the same name as a builtin.
|
||||
/// Checks for any class attributes or methods that use the same name as a
|
||||
/// builtin.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Reusing a builtin name for the name of an attribute increases the
|
||||
@@ -19,7 +22,9 @@ use crate::rules::flake8_builtins::helpers::shadows_builtin;
|
||||
///
|
||||
/// Builtins can be marked as exceptions to this rule via the
|
||||
/// [`flake8-builtins.builtins-ignorelist`] configuration option, or
|
||||
/// converted to the appropriate dunder method.
|
||||
/// converted to the appropriate dunder method. Methods decorated with
|
||||
/// `@typing.override` or `@typing_extensions.override` are also
|
||||
/// ignored.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -88,3 +93,59 @@ pub(crate) fn builtin_attribute_shadowing(
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A003
|
||||
pub(crate) fn builtin_method_shadowing(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
name: &str,
|
||||
decorator_list: &[Decorator],
|
||||
range: TextRange,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
|
||||
// those should be flagged on the superclass, but that's more difficult.
|
||||
if is_standard_library_override(name, class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore explicit overrides.
|
||||
if decorator_list.iter().any(|decorator| {
|
||||
checker
|
||||
.semantic()
|
||||
.match_typing_expr(&decorator.expression, "override")
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinAttributeShadowing {
|
||||
name: name.to_string(),
|
||||
},
|
||||
range,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if an attribute appears to be an override of a standard-library method.
|
||||
fn is_standard_library_override(
|
||||
name: &str,
|
||||
class_def: &ast::StmtClassDef,
|
||||
model: &SemanticModel,
|
||||
) -> bool {
|
||||
match name {
|
||||
// Ex) `Event#set`
|
||||
"set" => class_def.bases.iter().any(|base| {
|
||||
model.resolve_call_path(base).map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["threading", "Event"])
|
||||
})
|
||||
}),
|
||||
// Ex) `Filter#filter`
|
||||
"filter" => class_def.bases.iter().any(|base| {
|
||||
model.resolve_call_path(base).map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["logging", "Filter"])
|
||||
})
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,31 @@ A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
12 | pass
|
||||
|
|
||||
|
||||
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
|
|
||||
27 | ...
|
||||
28 |
|
||||
29 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
30 | ...
|
||||
|
|
||||
|
||||
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
|
|
||||
38 | ...
|
||||
39 |
|
||||
40 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
41 | ...
|
||||
|
|
||||
|
||||
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
||||
|
|
||||
50 | pass
|
||||
51 |
|
||||
52 | def int(self):
|
||||
| ^^^ A003
|
||||
53 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -19,4 +19,31 @@ A003.py:11:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
12 | pass
|
||||
|
|
||||
|
||||
A003.py:29:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
|
|
||||
27 | ...
|
||||
28 |
|
||||
29 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
30 | ...
|
||||
|
|
||||
|
||||
A003.py:40:9: A003 Class attribute `str` is shadowing a Python builtin
|
||||
|
|
||||
38 | ...
|
||||
39 |
|
||||
40 | def str(self) -> None:
|
||||
| ^^^ A003
|
||||
41 | ...
|
||||
|
|
||||
|
||||
A003.py:52:9: A003 Class attribute `int` is shadowing a Python builtin
|
||||
|
|
||||
50 | pass
|
||||
51 |
|
||||
52 | def int(self):
|
||||
| ^^^ A003
|
||||
53 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ pub(crate) fn fix_unnecessary_generator_set(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
|
||||
let module_text = locator.slice(expr.range());
|
||||
@@ -99,8 +99,8 @@ pub(crate) fn fix_unnecessary_generator_dict(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
let module_text = locator.slice(expr.range());
|
||||
let mut tree = match_expression(module_text)?;
|
||||
@@ -142,8 +142,8 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
// Expr(Call(ListComp)))) ->
|
||||
// Expr(SetComp)))
|
||||
let module_text = locator.slice(expr.range());
|
||||
@@ -178,8 +178,8 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
let module_text = locator.slice(expr.range());
|
||||
let mut tree = match_expression(module_text)?;
|
||||
@@ -265,8 +265,8 @@ pub(crate) fn fix_unnecessary_literal_set(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(List|Tuple)))) -> Expr(Set)))
|
||||
let module_text = locator.slice(expr.range());
|
||||
@@ -309,8 +309,8 @@ pub(crate) fn fix_unnecessary_literal_dict(
|
||||
checker: &Checker,
|
||||
expr: &rustpython_parser::ast::Expr,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(List|Tuple)))) -> Expr(Dict)))
|
||||
let module_text = locator.slice(expr.range());
|
||||
@@ -381,8 +381,8 @@ pub(crate) fn fix_unnecessary_collection_call(
|
||||
Dict,
|
||||
}
|
||||
|
||||
let locator = checker.locator;
|
||||
let stylist = checker.stylist;
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
|
||||
let module_text = locator.slice(expr.range());
|
||||
@@ -511,12 +511,12 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin
|
||||
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let prefix = checker.locator.up_to(range.start());
|
||||
let prefix = checker.locator().up_to(range.start());
|
||||
let left_pad = matches!(prefix.chars().next_back(), Some('{'));
|
||||
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let suffix = checker.locator.after(range.end());
|
||||
let suffix = checker.locator().after(range.end());
|
||||
let right_pad = matches!(suffix.chars().next(), Some('}'));
|
||||
|
||||
if left_pad && right_pad {
|
||||
|
||||
@@ -86,8 +86,11 @@ pub(crate) fn unnecessary_call_around_sorted(
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let edit =
|
||||
fixes::fix_unnecessary_call_around_sorted(checker.locator, checker.stylist, expr)?;
|
||||
let edit = fixes::fix_unnecessary_call_around_sorted(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)?;
|
||||
if outer == "reversed" {
|
||||
Ok(Fix::suggested(edit))
|
||||
} else {
|
||||
|
||||
@@ -68,7 +68,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_comprehension(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -84,7 +84,11 @@ pub(crate) fn unnecessary_comprehension_any_all(
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_comprehension_any_all(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_comprehension_any_all(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -135,8 +135,8 @@ pub(crate) fn unnecessary_double_cast_or_process(
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_double_cast_or_process(
|
||||
checker.locator,
|
||||
checker.stylist,
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(crate) fn unnecessary_generator_list(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_generator_list(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn unnecessary_list_call(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_list_call(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -93,7 +93,11 @@ pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_literal_within_dict_call(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_literal_within_dict_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -96,7 +96,11 @@ pub(crate) fn unnecessary_literal_within_list_call(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_literal_within_list_call(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_literal_within_list_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -97,7 +97,11 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.try_set_fix_from_edit(|| {
|
||||
fixes::fix_unnecessary_literal_within_tuple_call(checker.locator, checker.stylist, expr)
|
||||
fixes::fix_unnecessary_literal_within_tuple_call(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -163,8 +163,14 @@ pub(crate) fn unnecessary_map(
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_map(checker.locator, checker.stylist, expr, parent, object_type)
|
||||
.map(Fix::suggested)
|
||||
fixes::fix_unnecessary_map(
|
||||
checker.locator(),
|
||||
checker.stylist(),
|
||||
expr,
|
||||
parent,
|
||||
object_type,
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Expr, Keyword, Ranged};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::find_keyword;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -65,10 +66,7 @@ pub(crate) fn locals_in_render_function(
|
||||
return;
|
||||
}
|
||||
&args[2]
|
||||
} else if let Some(keyword) = keywords
|
||||
.iter()
|
||||
.find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg == "context"))
|
||||
{
|
||||
} else if let Some(keyword) = find_keyword(keywords, "context") {
|
||||
if !is_locals_call(&keyword.value, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,14 +188,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
Diagnostic::new(RawStringInException, first.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.locator, stmt)
|
||||
whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist,
|
||||
checker.stylist(),
|
||||
checker.generator(),
|
||||
));
|
||||
}
|
||||
@@ -211,14 +211,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.locator, stmt)
|
||||
whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist,
|
||||
checker.stylist(),
|
||||
checker.generator(),
|
||||
));
|
||||
}
|
||||
@@ -238,14 +238,14 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
|
||||
Diagnostic::new(DotFormatInException, first.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.locator, stmt)
|
||||
whitespace::indentation(checker.locator(), stmt)
|
||||
{
|
||||
if checker.semantic().is_available("msg") {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist,
|
||||
checker.stylist(),
|
||||
checker.generator(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ mod tests {
|
||||
#[test_case(Path::new("EXE004_1.py"))]
|
||||
#[test_case(Path::new("EXE004_2.py"))]
|
||||
#[test_case(Path::new("EXE004_3.py"))]
|
||||
#[test_case(Path::new("EXE004_4.py"))]
|
||||
#[test_case(Path::new("EXE005_1.py"))]
|
||||
#[test_case(Path::new("EXE005_2.py"))]
|
||||
#[test_case(Path::new("EXE005_3.py"))]
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
pub(crate) use shebang_missing::*;
|
||||
pub(crate) use shebang_newline::*;
|
||||
pub(crate) use shebang_not_executable::*;
|
||||
pub(crate) use shebang_python::*;
|
||||
pub(crate) use shebang_whitespace::*;
|
||||
use std::path::Path;
|
||||
|
||||
mod shebang_missing;
|
||||
mod shebang_newline;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
pub(crate) use shebang_leading_whitespace::*;
|
||||
pub(crate) use shebang_missing_executable_file::*;
|
||||
pub(crate) use shebang_missing_python::*;
|
||||
pub(crate) use shebang_not_executable::*;
|
||||
pub(crate) use shebang_not_first_line::*;
|
||||
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
use crate::settings::Settings;
|
||||
|
||||
mod shebang_leading_whitespace;
|
||||
mod shebang_missing_executable_file;
|
||||
mod shebang_missing_python;
|
||||
mod shebang_not_executable;
|
||||
mod shebang_python;
|
||||
mod shebang_whitespace;
|
||||
mod shebang_not_first_line;
|
||||
|
||||
pub(crate) fn from_tokens(
|
||||
tokens: &[LexResult],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
settings: &Settings,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let mut has_any_shebang = false;
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if let Tok::Comment(comment) = tok {
|
||||
if let Some(shebang) = ShebangDirective::try_extract(comment) {
|
||||
has_any_shebang = true;
|
||||
|
||||
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_not_executable(path, *range) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_any_shebang {
|
||||
if let Some(diagnostic) = shebang_missing_executable_file(path) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::ops::Sub;
|
||||
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_trivia::is_python_whitespace;
|
||||
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
use crate::registry::AsRule;
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for whitespace before a shebang directive.
|
||||
@@ -46,31 +47,29 @@ impl AlwaysAutofixableViolation for ShebangLeadingWhitespace {
|
||||
}
|
||||
|
||||
/// EXE004
|
||||
pub(crate) fn shebang_whitespace(
|
||||
pub(crate) fn shebang_leading_whitespace(
|
||||
range: TextRange,
|
||||
shebang: &ShebangDirective,
|
||||
autofix: bool,
|
||||
locator: &Locator,
|
||||
settings: &Settings,
|
||||
) -> Option<Diagnostic> {
|
||||
let ShebangDirective {
|
||||
offset,
|
||||
contents: _,
|
||||
} = shebang;
|
||||
|
||||
if *offset > TextSize::from(2) {
|
||||
let leading_space_start = range.start();
|
||||
let leading_space_len = offset.sub(TextSize::new(2));
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ShebangLeadingWhitespace,
|
||||
TextRange::at(leading_space_start, leading_space_len),
|
||||
);
|
||||
if autofix {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(TextRange::at(
|
||||
leading_space_start,
|
||||
leading_space_len,
|
||||
))));
|
||||
}
|
||||
Some(diagnostic)
|
||||
} else {
|
||||
None
|
||||
// If the shebang is at the beginning of the file, abort.
|
||||
if range.start() == TextSize::from(0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the entire prefix _isn't_ whitespace, abort (this is handled by EXE005).
|
||||
if !locator
|
||||
.up_to(range.start())
|
||||
.chars()
|
||||
.all(|c| is_python_whitespace(c) || matches!(c, '\r' | '\n'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let prefix = TextRange::up_to(range.start());
|
||||
let mut diagnostic = Diagnostic::new(ShebangLeadingWhitespace, prefix);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(prefix)));
|
||||
}
|
||||
Some(diagnostic)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user