Compare commits
126 Commits
evanritten
...
v0.0.286
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91880b8273 | ||
|
|
100904adb9 | ||
|
|
0bac7bd114 | ||
|
|
f2eb7bcacf | ||
|
|
29a0c1003b | ||
|
|
15b7525464 | ||
|
|
0b6dab5e3f | ||
|
|
1c66bb80b7 | ||
|
|
d1f07008f7 | ||
|
|
61b2ffa8e8 | ||
|
|
1044d66c1c | ||
|
|
813d7da7ec | ||
|
|
59e70896c0 | ||
|
|
f754ad5898 | ||
|
|
474e8fbcd4 | ||
|
|
6f23469e00 | ||
|
|
281ce56dc1 | ||
|
|
3bd199cdf6 | ||
|
|
948cd29b23 | ||
|
|
1e7d1968b1 | ||
|
|
8b46b71038 | ||
|
|
1cd7790a8a | ||
|
|
d376cb4c2a | ||
|
|
04a9a8dd03 | ||
|
|
e4c13846e3 | ||
|
|
205d234856 | ||
|
|
4889b84338 | ||
|
|
847432cacf | ||
|
|
9b6e008cf1 | ||
|
|
39c6665ff9 | ||
|
|
19a87c220a | ||
|
|
0688883404 | ||
|
|
3bb638875f | ||
|
|
26e63ab137 | ||
|
|
417a1d0717 | ||
|
|
34b2ae73b4 | ||
|
|
71c25e4f9d | ||
|
|
4bdd99f882 | ||
|
|
1e6d1182bf | ||
|
|
d08f697a04 | ||
|
|
4bc5eddf91 | ||
|
|
5f5de52aba | ||
|
|
1cb1bd731c | ||
|
|
db2e548f4f | ||
|
|
94f5f18ddb | ||
|
|
c34a342ab4 | ||
|
|
42ff833d00 | ||
|
|
e1f4438498 | ||
|
|
1acdec3e29 | ||
|
|
ca2bb20063 | ||
|
|
2e00983762 | ||
|
|
fb7caf43c8 | ||
|
|
e53bf25616 | ||
|
|
214eb707a6 | ||
|
|
5c1f7fd5dd | ||
|
|
cc278c24e2 | ||
|
|
558b56f8a8 | ||
|
|
749da6589a | ||
|
|
d2eace3377 | ||
|
|
ccac9681e1 | ||
|
|
b52cc84df6 | ||
|
|
fec6fc2fab | ||
|
|
ba4c27598a | ||
|
|
0f9ccfcad9 | ||
|
|
fa32cd9b6f | ||
|
|
0aad0c41f6 | ||
|
|
424b8d4ad2 | ||
|
|
abc5065fc7 | ||
|
|
37f4920e1e | ||
|
|
c0df99b965 | ||
|
|
7650c6ee45 | ||
|
|
7b14d17e39 | ||
|
|
4678f7dafe | ||
|
|
b182368008 | ||
|
|
e032fbd2e7 | ||
|
|
575b77aa52 | ||
|
|
17a26e6ff3 | ||
|
|
d5a51b4e45 | ||
|
|
83f68891e0 | ||
|
|
aafde6db28 | ||
|
|
2405536d03 | ||
|
|
f017555d53 | ||
|
|
be96e0041a | ||
|
|
3c2dd5e42e | ||
|
|
8b347cdaa9 | ||
|
|
2a8d24dd4b | ||
|
|
bb5fbb1b5c | ||
|
|
086e11087f | ||
|
|
1b7e4a12a9 | ||
|
|
da1697121e | ||
|
|
419615f29b | ||
|
|
a742a562fd | ||
|
|
129b19050a | ||
|
|
0dc23da1d0 | ||
|
|
c62e544cba | ||
|
|
7e9023b6f8 | ||
|
|
a489b96a65 | ||
|
|
17af12e57c | ||
|
|
648333b8b2 | ||
|
|
3849fa0cf1 | ||
|
|
59e533047a | ||
|
|
053b1145f0 | ||
|
|
6a5acde226 | ||
|
|
ea72d5feba | ||
|
|
2aeb27334d | ||
|
|
0cea4975fc | ||
|
|
3ceb6fbeb0 | ||
|
|
8e18f8018f | ||
|
|
8228429a70 | ||
|
|
1811312722 | ||
|
|
26bba11be6 | ||
|
|
a128fe5148 | ||
|
|
5892c691ea | ||
|
|
82e0a97b34 | ||
|
|
a8d7bbae6f | ||
|
|
1050142a58 | ||
|
|
db1c556508 | ||
|
|
a70807e1e1 | ||
|
|
d9bb51dee4 | ||
|
|
d0f2a8e424 | ||
|
|
1334232168 | ||
|
|
fa7442da2f | ||
|
|
4dc32a00d0 | ||
|
|
e3ecbe660e | ||
|
|
8c3a8c4fc6 | ||
|
|
dcc7226685 |
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: mkdocs build --strict -f mkdocs.generated.yml
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@3.0.0
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/playground.yaml
vendored
2
.github/workflows/playground.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@3.0.0
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.284"
|
||||
version = "0.0.286"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -876,8 +876,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2064,7 +2066,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.284"
|
||||
version = "0.0.286"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2128,6 +2130,7 @@ dependencies = [
|
||||
"typed-arena",
|
||||
"unicode-width",
|
||||
"unicode_names2",
|
||||
"uuid",
|
||||
"wsl",
|
||||
]
|
||||
|
||||
@@ -2164,7 +2167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.284"
|
||||
version = "0.0.286"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2340,6 +2343,7 @@ dependencies = [
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"ruff_formatter",
|
||||
"ruff_python_ast",
|
||||
@@ -2399,6 +2403,7 @@ dependencies = [
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"static_assertions",
|
||||
"test-case",
|
||||
"tiny-keccak",
|
||||
"unic-emoji-char",
|
||||
"unic-ucd-ident",
|
||||
@@ -3344,9 +3349,26 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
"uuid-macro-internal",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
|
||||
@@ -50,6 +50,7 @@ tracing = "0.1.37"
|
||||
tracing-indicatif = "0.3.4"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
unicode-width = "0.1.10"
|
||||
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
# v1.0.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.284
|
||||
rev: v0.0.286
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.284"
|
||||
version = "0.0.286"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.284"
|
||||
version = "0.0.286"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -77,6 +77,7 @@ toml = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -19,3 +19,12 @@ def foo(x, y, z):
|
||||
class A():
|
||||
pass
|
||||
# b = c
|
||||
|
||||
|
||||
dictionary = {
|
||||
# "key1": 123, # noqa: ERA001
|
||||
# "key2": 456,
|
||||
# "key3": 789, # test
|
||||
}
|
||||
|
||||
#import os # noqa
|
||||
|
||||
@@ -152,3 +152,9 @@ def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
def f(a: Optional[Any]) -> None: ...
|
||||
def f(a: Annotated[Any, ...]) -> None: ...
|
||||
def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
|
||||
|
||||
class Foo:
|
||||
@decorator()
|
||||
def __init__(self: "Foo", foo: int):
|
||||
...
|
||||
|
||||
@@ -69,6 +69,7 @@ g_action.set_enabled(True)
|
||||
settings.set_enable_developer_extras(True)
|
||||
foo.is_(True)
|
||||
bar.is_not(False)
|
||||
next(iter([]), False)
|
||||
|
||||
class Registry:
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -230,6 +230,10 @@ def timedelta_okay(value=dt.timedelta(hours=1)):
|
||||
def path_okay(value=Path(".")):
|
||||
pass
|
||||
|
||||
# B008 allow arbitrary call with immutable annotation
|
||||
def immutable_annotation_call(value: Sequence[int] = foo()):
|
||||
pass
|
||||
|
||||
# B006 and B008
|
||||
# We should handle arbitrary nesting of these B008.
|
||||
def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
|
||||
18
crates/ruff/resources/test/fixtures/flake8_bugbear/B006_extended.py
vendored
Normal file
18
crates/ruff/resources/test/fixtures/flake8_bugbear/B006_extended.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import custom
|
||||
from custom import ImmutableTypeB
|
||||
|
||||
|
||||
def okay(foo: ImmutableTypeB = []):
|
||||
...
|
||||
|
||||
|
||||
def okay(foo: custom.ImmutableTypeA = []):
|
||||
...
|
||||
|
||||
|
||||
def okay(foo: custom.ImmutableTypeB = []):
|
||||
...
|
||||
|
||||
|
||||
def error_due_to_missing_import(foo: ImmutableTypeA = []):
|
||||
...
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import List
|
||||
|
||||
import fastapi
|
||||
import custom
|
||||
from fastapi import Query
|
||||
|
||||
|
||||
@@ -16,5 +17,9 @@ def okay(data: List[str] = Query(None)):
|
||||
...
|
||||
|
||||
|
||||
def okay(data: custom.ImmutableTypeA = foo()):
|
||||
...
|
||||
|
||||
|
||||
def error_due_to_missing_import(data: List[str] = Depends(None)):
|
||||
...
|
||||
|
||||
@@ -22,6 +22,10 @@ tuple(
|
||||
"o"]
|
||||
)
|
||||
)
|
||||
set(set())
|
||||
set(list())
|
||||
set(tuple())
|
||||
sorted(reversed())
|
||||
|
||||
# Nested sorts with differing keyword arguments. Not flagged.
|
||||
sorted(sorted(x, key=lambda y: y))
|
||||
|
||||
@@ -15,11 +15,6 @@ filter(func, map(lambda v: v, nums))
|
||||
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
|
||||
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
||||
|
||||
# Error, but unfixable.
|
||||
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
|
||||
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
|
||||
map(lambda x=1: x, nums)
|
||||
|
||||
# False negatives.
|
||||
map(lambda x=2, y=1: x + y, nums, nums)
|
||||
set(map(lambda x, y: x, nums, nums))
|
||||
@@ -37,3 +32,8 @@ map(lambda x: lambda: x, range(4))
|
||||
|
||||
# Error: the `x` is overridden by the inner lambda.
|
||||
map(lambda x: lambda x: x, range(4))
|
||||
|
||||
# Ok because of the default parameters, and variadic arguments.
|
||||
map(lambda x=1: x, nums)
|
||||
map(lambda *args: len(args), range(4))
|
||||
map(lambda **kwargs: len(kwargs), range(4))
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import math # not checked
|
||||
def not_checked():
|
||||
import math
|
||||
|
||||
import altair # unconventional
|
||||
import matplotlib.pyplot # unconventional
|
||||
import numpy # unconventional
|
||||
import pandas # unconventional
|
||||
import seaborn # unconventional
|
||||
import tkinter # unconventional
|
||||
|
||||
import altair as altr # unconventional
|
||||
import matplotlib.pyplot as plot # unconventional
|
||||
import numpy as nmp # unconventional
|
||||
import pandas as pdas # unconventional
|
||||
import seaborn as sbrn # unconventional
|
||||
import tkinter as tkr # unconventional
|
||||
def unconventional():
|
||||
import altair
|
||||
import matplotlib.pyplot
|
||||
import numpy
|
||||
import pandas
|
||||
import seaborn
|
||||
import tkinter
|
||||
import networkx
|
||||
|
||||
import altair as alt # conventional
|
||||
import matplotlib.pyplot as plt # conventional
|
||||
import numpy as np # conventional
|
||||
import pandas as pd # conventional
|
||||
import seaborn as sns # conventional
|
||||
import tkinter as tk # conventional
|
||||
|
||||
def unconventional_aliases():
|
||||
import altair as altr
|
||||
import matplotlib.pyplot as plot
|
||||
import numpy as nmp
|
||||
import pandas as pdas
|
||||
import seaborn as sbrn
|
||||
import tkinter as tkr
|
||||
import networkx as nxy
|
||||
|
||||
def conventional_aliases():
|
||||
import altair as alt
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
import tkinter as tk
|
||||
import networkx as nx
|
||||
|
||||
13
crates/ruff/resources/test/fixtures/flake8_pie/PIE808.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/flake8_pie/PIE808.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# PIE808
|
||||
range(0, 10)
|
||||
|
||||
# OK
|
||||
range(x, 10)
|
||||
range(-15, 10)
|
||||
range(10)
|
||||
range(0)
|
||||
range(0, 10, x)
|
||||
range(0, 10, 1)
|
||||
range(0, 10, step=1)
|
||||
range(start=0, stop=10)
|
||||
range(0, stop=10)
|
||||
@@ -64,3 +64,8 @@ def test_implicit_str_concat_no_parens(param1, param2, param3):
|
||||
@pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
|
||||
def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
def test_csv_with_parens(param1, param2):
|
||||
...
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from pickle import PicklingError, UnpicklingError
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
@@ -20,6 +21,12 @@ def test_error_no_argument_given():
|
||||
with pytest.raises(socket.error):
|
||||
raise ValueError("Can't divide 1 by 0")
|
||||
|
||||
with pytest.raises(PicklingError):
|
||||
raise PicklingError("Can't pickle")
|
||||
|
||||
with pytest.raises(UnpicklingError):
|
||||
raise UnpicklingError("Can't unpickle")
|
||||
|
||||
|
||||
def test_error_match_is_empty():
|
||||
with pytest.raises(ValueError, match=None):
|
||||
|
||||
@@ -16,11 +16,38 @@ def test_error_expr_simple(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
|
||||
@pytest.mark.parametrize(
|
||||
"x",
|
||||
[
|
||||
(a, b),
|
||||
# comment
|
||||
(a, b),
|
||||
(b, c),
|
||||
],
|
||||
)
|
||||
def test_error_expr_complex(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
|
||||
def test_error_parentheses(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"x",
|
||||
[
|
||||
a,
|
||||
b,
|
||||
(a),
|
||||
c,
|
||||
((a)),
|
||||
],
|
||||
)
|
||||
def test_error_parentheses_trailing_comma(x):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x", [1, 2])
|
||||
def test_ok(x):
|
||||
...
|
||||
|
||||
@@ -43,3 +43,12 @@ message
|
||||
assert something # OK
|
||||
assert something and something_else # Error
|
||||
assert something and something_else and something_third # Error
|
||||
|
||||
|
||||
def test_multiline():
|
||||
assert something and something_else; x = 1
|
||||
|
||||
x = 1; assert something and something_else
|
||||
|
||||
x = 1; \
|
||||
assert something and something_else
|
||||
|
||||
@@ -19,11 +19,20 @@ raise TypeError ()
|
||||
raise TypeError \
|
||||
()
|
||||
|
||||
# RSE102
|
||||
raise TypeError \
|
||||
();
|
||||
|
||||
# RSE102
|
||||
raise TypeError(
|
||||
|
||||
)
|
||||
|
||||
# RSE102
|
||||
raise (TypeError) (
|
||||
|
||||
)
|
||||
|
||||
# RSE102
|
||||
raise TypeError(
|
||||
# Hello, world!
|
||||
@@ -52,3 +61,10 @@ class Class:
|
||||
|
||||
# OK
|
||||
raise Class.error()
|
||||
|
||||
|
||||
import ctypes
|
||||
|
||||
|
||||
# OK
|
||||
raise ctypes.WinError(1)
|
||||
|
||||
@@ -320,3 +320,9 @@ def end_of_statement():
|
||||
if True:
|
||||
return "" \
|
||||
; # type: ignore
|
||||
|
||||
|
||||
def end_of_file():
|
||||
if False:
|
||||
return 1
|
||||
x = 2 \
|
||||
|
||||
@@ -31,6 +31,8 @@ for key in list(obj.keys()):
|
||||
|
||||
key in (obj or {}).keys() # SIM118
|
||||
|
||||
(key) in (obj or {}).keys() # SIM118
|
||||
|
||||
from typing import KeysView
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ def f(cls, x):
|
||||
###
|
||||
lambda x: print("Hello, world!")
|
||||
|
||||
lambda: print("Hello, world!")
|
||||
|
||||
|
||||
class C:
|
||||
###
|
||||
|
||||
0
crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/__init__.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/bar.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/bar.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
import pandas
|
||||
import foo.baz
|
||||
2
crates/ruff/resources/test/fixtures/isort/detect_same_package/pyproject.toml
vendored
Normal file
2
crates/ruff/resources/test/fixtures/isort/detect_same_package/pyproject.toml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
37
crates/ruff/resources/test/fixtures/jupyter/add_missing_cell_id.ipynb
vendored
Normal file
37
crates/ruff/resources/test/fixtures/jupyter/add_missing_cell_id.ipynb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import math\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"math.pi"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff)",
|
||||
"language": "python",
|
||||
"name": "ruff"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
37
crates/ruff/resources/test/fixtures/jupyter/no_cell_id.ipynb
vendored
Normal file
37
crates/ruff/resources/test/fixtures/jupyter/no_cell_id.ipynb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import math\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"math.pi"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (ruff)",
|
||||
"language": "python",
|
||||
"name": "ruff"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -28,3 +28,6 @@ mdtypes_template = {
|
||||
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
|
||||
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
|
||||
}
|
||||
|
||||
#: Okay
|
||||
a = (1,
|
||||
|
||||
@@ -25,6 +25,12 @@ if (True) == TrueElement or x == TrueElement:
|
||||
if res == True != False:
|
||||
pass
|
||||
|
||||
if(True) == TrueElement or x == TrueElement:
|
||||
pass
|
||||
|
||||
if (yield i) == True:
|
||||
print("even")
|
||||
|
||||
#: Okay
|
||||
if x not in y:
|
||||
pass
|
||||
|
||||
@@ -133,3 +133,8 @@ def scope():
|
||||
from collections.abc import Callable
|
||||
|
||||
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
|
||||
|
||||
|
||||
class TemperatureScales(Enum):
|
||||
CELSIUS = (lambda deg_c: deg_c)
|
||||
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)
|
||||
|
||||
13
crates/ruff/resources/test/fixtures/pydocstyle/D200.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/pydocstyle/D200.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
def func():
|
||||
"""\
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""\\
|
||||
"""
|
||||
|
||||
|
||||
def func():
|
||||
"""\ \
|
||||
"""
|
||||
@@ -99,3 +99,16 @@ import foo.bar as bop
|
||||
import foo.bar.baz
|
||||
|
||||
print(bop.baz.read_csv("test.csv"))
|
||||
|
||||
# Test: isolated deletions.
|
||||
if TYPE_CHECKING:
|
||||
import a1
|
||||
|
||||
import a2
|
||||
|
||||
|
||||
match *0, 1, *2:
|
||||
case 0,:
|
||||
import b1
|
||||
|
||||
import b2
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
"{bar}{}".format(1, bar=2, spam=3) # F522
|
||||
"{bar:{spam}}".format(bar=2, spam=3) # No issues
|
||||
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
|
||||
# Not fixable
|
||||
(''
|
||||
.format(x=2))
|
||||
.format(x=2)) # F522
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
|
||||
"{1} {8}".format(0, 1) # F523, # F524
|
||||
|
||||
# Not fixable
|
||||
# Multiline
|
||||
(''
|
||||
.format(2))
|
||||
|
||||
@@ -154,3 +154,14 @@ def f() -> None:
|
||||
print("hello")
|
||||
except A as e :
|
||||
print("oh no!")
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
y = 2
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
|
||||
y = 2
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
|
||||
"{:s} {:y}".format("hello", "world") # [bad-format-character]
|
||||
|
||||
"{:*^30s}".format("centered")
|
||||
"{:*^30s}".format("centered") # OK
|
||||
"{:{s}}".format("hello", s="s") # OK (nested placeholder value not checked)
|
||||
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested placeholder format spec checked)
|
||||
"{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder)
|
||||
"{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders)
|
||||
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested placeholders are consumed without considering in between chars)
|
||||
|
||||
## f-strings
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
class Person:
|
||||
class Person: # [eq-without-hash]
|
||||
def __init__(self):
|
||||
self.name = "monty"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Person) and other.name == self.name
|
||||
|
||||
# OK
|
||||
class Language:
|
||||
def __init__(self):
|
||||
self.name = "python"
|
||||
@@ -14,3 +15,9 @@ class Language:
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
class MyClass:
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
__hash__ = None
|
||||
|
||||
62
crates/ruff/resources/test/fixtures/pylint/no_self_use.py
vendored
Normal file
62
crates/ruff/resources/test/fixtures/pylint/no_self_use.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import abc
|
||||
|
||||
|
||||
class Person:
|
||||
def developer_greeting(self, name): # [no-self-use]
|
||||
print(f"Greetings {name}!")
|
||||
|
||||
def greeting_1(self): # [no-self-use]
|
||||
print("Hello!")
|
||||
|
||||
def greeting_2(self): # [no-self-use]
|
||||
print("Hi!")
|
||||
|
||||
|
||||
# OK
|
||||
def developer_greeting():
|
||||
print("Greetings developer!")
|
||||
|
||||
|
||||
# OK
|
||||
class Person:
|
||||
name = "Paris"
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __cmp__(self, other):
|
||||
print(24)
|
||||
|
||||
def __repr__(self):
|
||||
return "Person"
|
||||
|
||||
def func(self):
|
||||
...
|
||||
|
||||
def greeting_1(self):
|
||||
print(f"Hello from {self.name} !")
|
||||
|
||||
@staticmethod
|
||||
def greeting_2():
|
||||
print("Hi!")
|
||||
|
||||
|
||||
class Base(abc.ABC):
|
||||
"""abstract class"""
|
||||
|
||||
@abstractmethod
|
||||
def abstract_method(self):
|
||||
"""abstract method could not be a function"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Sub(Base):
|
||||
@override
|
||||
def abstract_method(self):
|
||||
print("concret method")
|
||||
|
||||
|
||||
class Prop:
|
||||
@property
|
||||
def count(self):
|
||||
return 24
|
||||
@@ -9,13 +9,22 @@ foo != "a" and foo != "b" and foo != "c"
|
||||
|
||||
foo == a or foo == "b" or foo == 3 # Mixed types.
|
||||
|
||||
# False negatives (the current implementation doesn't support Yoda conditions).
|
||||
"a" == foo or "b" == foo or "c" == foo
|
||||
|
||||
"a" != foo and "b" != foo and "c" != foo
|
||||
|
||||
"a" == foo or foo == "b" or "c" == foo
|
||||
|
||||
foo == bar or baz == foo or qux == foo
|
||||
|
||||
foo == "a" or "b" == foo or foo == "c"
|
||||
|
||||
foo != "a" and "b" != foo and foo != "c"
|
||||
|
||||
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
|
||||
|
||||
foo.bar == "a" or foo.bar == "b" # Attributes.
|
||||
|
||||
# OK
|
||||
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
|
||||
|
||||
@@ -32,3 +41,13 @@ foo not in {"a", "b", "c"} # Uses membership test already.
|
||||
foo == "a" # Single comparison.
|
||||
|
||||
foo != "a" # Single comparison.
|
||||
|
||||
foo == "a" == "b" or foo == "c" # Multiple comparisons.
|
||||
|
||||
foo == bar == "b" or foo == "c" # Multiple comparisons.
|
||||
|
||||
foo == foo or foo == bar # Self-comparison.
|
||||
|
||||
foo[0] == "a" or foo[0] == "b" # Subscripts.
|
||||
|
||||
foo() == "a" or foo() == "b" # Calls.
|
||||
|
||||
3
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_11.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_11.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from sys import *
|
||||
|
||||
exit(0)
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_10.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_10.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
# coding=utf8""" # empty comment
|
||||
|
||||
"""
|
||||
Invalid coding declaration since it is nested inside a docstring
|
||||
The following empty comment tests for false positives as our implementation visits comments
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_6.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_6.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Regression test for https://github.com/astral-sh/ruff/issues/6756
|
||||
The leading space must be removed to prevent invalid syntax.
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_7.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_7.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Regression test for https://github.com/astral-sh/ruff/issues/6756
|
||||
The leading tab must be removed to prevent invalid syntax.
|
||||
"""
|
||||
6
crates/ruff/resources/test/fixtures/pyupgrade/UP009_8.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/pyupgrade/UP009_8.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
print("foo") # coding=utf8
|
||||
print("Hello world")
|
||||
|
||||
"""
|
||||
Invalid coding declaration due to a statement before the comment
|
||||
"""
|
||||
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_9.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/pyupgrade/UP009_9.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
x = 1 \
|
||||
# coding=utf8
|
||||
x = 2
|
||||
|
||||
"""
|
||||
Invalid coding declaration due to continuation on preceding line
|
||||
"""
|
||||
@@ -31,6 +31,7 @@ bool("foo")
|
||||
bool("")
|
||||
bool(b"")
|
||||
bool(1.0)
|
||||
int().denominator
|
||||
|
||||
# These become string or byte literals
|
||||
str()
|
||||
@@ -49,3 +50,6 @@ float(1.0)
|
||||
bool()
|
||||
bool(True)
|
||||
bool(False)
|
||||
|
||||
# These become a literal but retain parentheses
|
||||
int(1).denominator
|
||||
|
||||
14
crates/ruff/resources/test/fixtures/ruff/RUF017.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/ruff/RUF017.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
x = [1, 2, 3]
|
||||
y = [4, 5, 6]
|
||||
|
||||
# RUF017
|
||||
sum([x, y], start=[])
|
||||
sum([x, y], [])
|
||||
sum([[1, 2, 3], [4, 5, 6]], start=[])
|
||||
sum([[1, 2, 3], [4, 5, 6]], [])
|
||||
sum([[1, 2, 3], [4, 5, 6]],
|
||||
[])
|
||||
|
||||
# OK
|
||||
sum([x, y])
|
||||
sum([[1, 2, 3], [4, 5, 6]])
|
||||
11
crates/ruff/resources/test/fixtures/ruff/RUF100_5.py
vendored
Normal file
11
crates/ruff/resources/test/fixtures/ruff/RUF100_5.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
#import os # noqa
|
||||
#import os # noqa: ERA001
|
||||
|
||||
dictionary = {
|
||||
# "key1": 123, # noqa: ERA001
|
||||
# "key2": 456, # noqa
|
||||
# "key3": 789,
|
||||
}
|
||||
|
||||
|
||||
#import os # noqa: E501
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
|
||||
};
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{lexer, AsMode};
|
||||
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
|
||||
|
||||
use ruff_python_trivia::{
|
||||
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
|
||||
};
|
||||
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::autofix::codemods;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub(crate) fn delete_stmt(
|
||||
} else if has_leading_content(stmt.start(), locator) {
|
||||
Edit::range_deletion(stmt.range())
|
||||
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
|
||||
Edit::range_deletion(TextRange::new(start, stmt.end()))
|
||||
Edit::deletion(start, stmt.end())
|
||||
} else {
|
||||
let range = locator.full_lines_range(stmt.range());
|
||||
Edit::range_deletion(range)
|
||||
@@ -89,83 +89,52 @@ pub(crate) fn remove_argument<T: Ranged>(
|
||||
argument: &T,
|
||||
arguments: &Arguments,
|
||||
parentheses: Parentheses,
|
||||
locator: &Locator,
|
||||
source_type: PySourceType,
|
||||
source: &str,
|
||||
) -> Result<Edit> {
|
||||
// TODO(sbrugman): Preserve trailing comments.
|
||||
if arguments.keywords.len() + arguments.args.len() > 1 {
|
||||
let mut fix_start = None;
|
||||
let mut fix_end = None;
|
||||
// Partition into arguments before and after the argument to remove.
|
||||
let (before, after): (Vec<_>, Vec<_>) = arguments
|
||||
.args
|
||||
.iter()
|
||||
.map(Expr::range)
|
||||
.chain(arguments.keywords.iter().map(Keyword::range))
|
||||
.filter(|range| argument.range() != *range)
|
||||
.partition(|range| range.start() < argument.start());
|
||||
|
||||
if arguments
|
||||
.args
|
||||
.iter()
|
||||
.map(Expr::start)
|
||||
.chain(arguments.keywords.iter().map(Keyword::start))
|
||||
.any(|location| location > argument.start())
|
||||
{
|
||||
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
|
||||
// argument to the end of the subsequent comma.
|
||||
let mut seen_comma = false;
|
||||
for (tok, range) in lexer::lex_starts_at(
|
||||
locator.slice(arguments.range()),
|
||||
source_type.as_mode(),
|
||||
arguments.start(),
|
||||
)
|
||||
.flatten()
|
||||
{
|
||||
if seen_comma {
|
||||
if tok.is_non_logical_newline() {
|
||||
// Also delete any non-logical newlines after the comma.
|
||||
continue;
|
||||
}
|
||||
fix_end = Some(if tok.is_newline() {
|
||||
range.end()
|
||||
} else {
|
||||
range.start()
|
||||
});
|
||||
break;
|
||||
}
|
||||
if range.start() == argument.start() {
|
||||
fix_start = Some(range.start());
|
||||
}
|
||||
if fix_start.is_some() && tok.is_comma() {
|
||||
seen_comma = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Case 2: argument or keyword is the last node, so delete from the start of the
|
||||
// previous comma to the end of the argument.
|
||||
for (tok, range) in lexer::lex_starts_at(
|
||||
locator.slice(arguments.range()),
|
||||
source_type.as_mode(),
|
||||
arguments.start(),
|
||||
)
|
||||
.flatten()
|
||||
{
|
||||
if range.start() == argument.start() {
|
||||
fix_end = Some(argument.end());
|
||||
break;
|
||||
}
|
||||
if tok.is_comma() {
|
||||
fix_start = Some(range.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !after.is_empty() {
|
||||
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
|
||||
// argument to the end of the subsequent comma.
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(argument.end(), source);
|
||||
|
||||
match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
|
||||
_ => {
|
||||
bail!("No fix could be constructed")
|
||||
}
|
||||
}
|
||||
// Find the trailing comma.
|
||||
tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.context("Unable to find trailing comma")?;
|
||||
|
||||
// Find the next non-whitespace token.
|
||||
let next = tokenizer
|
||||
.find(|token| {
|
||||
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
|
||||
})
|
||||
.context("Unable to find next token")?;
|
||||
|
||||
Ok(Edit::deletion(argument.start(), next.start()))
|
||||
} else if let Some(previous) = before.iter().map(Ranged::end).max() {
|
||||
// Case 2: argument or keyword is the last node, so delete from the start of the
|
||||
// previous comma to the end of the argument.
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(previous, source);
|
||||
|
||||
// Find the trailing comma.
|
||||
let comma = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.context("Unable to find trailing comma")?;
|
||||
|
||||
Ok(Edit::deletion(comma.start(), argument.end()))
|
||||
} else {
|
||||
// Only one argument; remove it (but preserve parentheses, if needed).
|
||||
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
|
||||
// parentheses, if needed).
|
||||
Ok(match parentheses {
|
||||
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
|
||||
Parentheses::Preserve => {
|
||||
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
|
||||
}
|
||||
Parentheses::Remove => Edit::range_deletion(arguments.range()),
|
||||
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -257,25 +226,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
|
||||
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
|
||||
let start_location = semicolon + TextSize::from(1);
|
||||
|
||||
let contents = &locator.contents()[usize::from(start_location)..];
|
||||
for line in NewlineWithTrailingNewline::from(contents) {
|
||||
for line in
|
||||
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
|
||||
{
|
||||
let trimmed = line.trim_whitespace();
|
||||
// Skip past any continuations.
|
||||
if trimmed.starts_with('\\') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return start_location
|
||||
+ if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
line.start()
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
|
||||
line.start() + TextSize::try_from(relative_offset).unwrap()
|
||||
};
|
||||
return if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
line.start()
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
|
||||
line.start() + TextSize::try_from(relative_offset).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
locator.line_end(start_location)
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::registry::{AsRule, Rule};
|
||||
|
||||
pub(crate) mod codemods;
|
||||
pub(crate) mod edits;
|
||||
pub(crate) mod snippet;
|
||||
pub(crate) mod source_map;
|
||||
|
||||
pub(crate) struct FixResult {
|
||||
|
||||
36
crates/ruff/src/autofix/snippet.rs
Normal file
36
crates/ruff/src/autofix/snippet.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A snippet of source code for user-facing display, as in a diagnostic.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct SourceCodeSnippet(String);
|
||||
|
||||
impl SourceCodeSnippet {
|
||||
pub(crate) fn new(source_code: String) -> Self {
|
||||
Self(source_code)
|
||||
}
|
||||
|
||||
/// Return the full snippet for user-facing display, or `None` if the snippet should be
|
||||
/// truncated.
|
||||
pub(crate) fn full_display(&self) -> Option<&str> {
|
||||
if Self::should_truncate(&self.0) {
|
||||
None
|
||||
} else {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a truncated snippet for user-facing display.
|
||||
pub(crate) fn truncated_display(&self) -> &str {
|
||||
if Self::should_truncate(&self.0) {
|
||||
"..."
|
||||
} else {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the source code should be truncated when included in a user-facing
|
||||
/// diagnostic.
|
||||
fn should_truncate(source_code: &str) -> bool {
|
||||
source_code.width() > 50 || source_code.contains(['\r', '\n'])
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_python_ast::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
@@ -29,7 +30,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
pyflakes::rules::UnusedVariable {
|
||||
name: binding.name(checker.locator).to_string(),
|
||||
},
|
||||
binding.range,
|
||||
binding.range(),
|
||||
);
|
||||
if checker.patch(Rule::UnusedVariable) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
|
||||
@@ -7,10 +7,6 @@ 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,
|
||||
);
|
||||
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bugbear, perflint};
|
||||
use crate::rules::{flake8_bugbear, perflint, pyupgrade};
|
||||
|
||||
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
|
||||
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
|
||||
@@ -11,18 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
|
||||
for snapshot in for_loops {
|
||||
checker.semantic.restore(snapshot);
|
||||
|
||||
let Stmt::For(ast::StmtFor {
|
||||
target, iter, body, ..
|
||||
}) = checker.semantic.current_statement()
|
||||
else {
|
||||
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
|
||||
unreachable!("Expected Stmt::For");
|
||||
};
|
||||
|
||||
if checker.enabled(Rule::UnusedLoopControlVariable) {
|
||||
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
|
||||
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
|
||||
}
|
||||
if checker.enabled(Rule::IncorrectDictIterator) {
|
||||
perflint::rules::incorrect_dict_iterator(checker, target, iter);
|
||||
perflint::rules::incorrect_dict_iterator(checker, stmt_for);
|
||||
}
|
||||
if checker.enabled(Rule::YieldInForLoop) {
|
||||
pyupgrade::rules::yield_in_for_loop(checker, stmt_for);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_semantic::analyze::{branch_detection, visibility};
|
||||
use ruff_python_ast::Ranged;
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -29,6 +30,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::UnusedPrivateTypedDict,
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
Rule::UnusedVariable,
|
||||
Rule::NoSelfUse,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +83,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
pylint::rules::GlobalVariableNotAssigned {
|
||||
name: (*name).to_string(),
|
||||
},
|
||||
binding.range,
|
||||
binding.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -111,25 +113,21 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
branch_detection::different_forks(
|
||||
left,
|
||||
right,
|
||||
checker.semantic.statements(),
|
||||
)
|
||||
checker.semantic.different_branches(left, right)
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let line = checker.locator.compute_line_index(shadowed.range.start());
|
||||
let line = checker.locator.compute_line_index(shadowed.start());
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::ImportShadowedByLoopVar {
|
||||
name: name.to_string(),
|
||||
line,
|
||||
},
|
||||
binding.range,
|
||||
binding.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -171,7 +169,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(statement_id) = shadowed.source else {
|
||||
let Some(node_id) = shadowed.source else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -179,7 +177,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
if shadowed.kind.is_function_definition() {
|
||||
if checker
|
||||
.semantic
|
||||
.statement(statement_id)
|
||||
.statement(node_id)
|
||||
.as_function_def_stmt()
|
||||
.is_some_and(|function| {
|
||||
visibility::is_overload(
|
||||
@@ -207,24 +205,20 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
// If the bindings are in different forks, abort.
|
||||
if shadowed.source.map_or(true, |left| {
|
||||
binding.source.map_or(true, |right| {
|
||||
branch_detection::different_forks(
|
||||
left,
|
||||
right,
|
||||
checker.semantic.statements(),
|
||||
)
|
||||
checker.semantic.different_branches(left, right)
|
||||
})
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let line = checker.locator.compute_line_index(shadowed.range.start());
|
||||
let line = checker.locator.compute_line_index(shadowed.start());
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
pyflakes::rules::RedefinedWhileUnused {
|
||||
name: (*name).to_string(),
|
||||
line,
|
||||
},
|
||||
binding.range,
|
||||
binding.range(),
|
||||
);
|
||||
if let Some(range) = binding.parent_range(&checker.semantic) {
|
||||
diagnostic.set_parent(range.start());
|
||||
@@ -309,6 +303,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
if scope.kind.is_function() {
|
||||
if checker.enabled(Rule::NoSelfUse) {
|
||||
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
}
|
||||
checker.diagnostics.extend(diagnostics);
|
||||
}
|
||||
|
||||
@@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
Ok(summary) => {
|
||||
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
|
||||
pyflakes::rules::string_dot_format_extra_named_arguments(
|
||||
checker, &summary, keywords, location,
|
||||
checker, call, &summary, keywords,
|
||||
);
|
||||
}
|
||||
if checker
|
||||
.enabled(Rule::StringDotFormatExtraPositionalArguments)
|
||||
{
|
||||
pyflakes::rules::string_dot_format_extra_positional_arguments(
|
||||
checker, &summary, args, location,
|
||||
checker, call, &summary, args,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::StringDotFormatMissingArguments) {
|
||||
pyflakes::rules::string_dot_format_missing_argument(
|
||||
checker, &summary, args, keywords, location,
|
||||
checker, call, &summary, args, keywords,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
|
||||
pyflakes::rules::string_dot_format_mixing_automatic(
|
||||
checker, &summary, location,
|
||||
checker, call, &summary,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::FormatLiterals) {
|
||||
pyupgrade::rules::format_literals(checker, &summary, call);
|
||||
pyupgrade::rules::format_literals(checker, call, &summary);
|
||||
}
|
||||
if checker.enabled(Rule::FString) {
|
||||
pyupgrade::rules::f_strings(
|
||||
checker,
|
||||
call,
|
||||
&summary,
|
||||
expr,
|
||||
value,
|
||||
checker.settings.line_length,
|
||||
);
|
||||
@@ -432,7 +432,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::deprecated_unittest_alias(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::SuperCallWithParameters) {
|
||||
pyupgrade::rules::super_call_with_parameters(checker, expr, func, args);
|
||||
pyupgrade::rules::super_call_with_parameters(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryEncodeUTF8) {
|
||||
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
|
||||
@@ -441,7 +441,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::redundant_open_modes(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::NativeLiterals) {
|
||||
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
|
||||
pyupgrade::rules::native_literals(
|
||||
checker,
|
||||
call,
|
||||
checker.semantic().current_expression_parent(),
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::OpenAlias) {
|
||||
pyupgrade::rules::open_alias(checker, expr, func);
|
||||
@@ -531,6 +535,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryDictKwargs) {
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryRangeStart) {
|
||||
flake8_pie::rules::unnecessary_range_start(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::ExecBuiltin) {
|
||||
flake8_bandit::rules::exec_used(checker, func);
|
||||
}
|
||||
@@ -873,6 +880,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
|
||||
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::QuadraticListSummation) {
|
||||
ruff::rules::quadratic_list_summation(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(ast::ExprDict {
|
||||
keys,
|
||||
@@ -1172,7 +1182,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pylint::rules::magic_value_comparison(checker, left, comparators);
|
||||
}
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_compare(checker, expr, left, ops, comparators);
|
||||
flake8_simplify::rules::key_in_dict_compare(checker, compare);
|
||||
}
|
||||
if checker.enabled(Rule::YodaConditions) {
|
||||
flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators);
|
||||
|
||||
@@ -338,9 +338,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::FStringDocstring) {
|
||||
flake8_bugbear::rules::f_string_docstring(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::YieldInForLoop) {
|
||||
pyupgrade::rules::yield_in_for_loop(checker, stmt);
|
||||
}
|
||||
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
|
||||
if checker.enabled(Rule::BuiltinAttributeShadowing) {
|
||||
flake8_builtins::rules::builtin_method_shadowing(
|
||||
@@ -467,17 +464,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_pyi::rules::pass_statement_stub_body(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::PassInClassBody) {
|
||||
flake8_pyi::rules::pass_in_class_body(checker, stmt, body);
|
||||
flake8_pyi::rules::pass_in_class_body(checker, class_def);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
|
||||
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
|
||||
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) {
|
||||
flake8_pytest_style::rules::marks(checker, decorator_list);
|
||||
}
|
||||
if checker.enabled(Rule::DuplicateClassFieldDefinition) {
|
||||
flake8_pie::rules::duplicate_class_field_definition(checker, stmt, body);
|
||||
flake8_pie::rules::duplicate_class_field_definition(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::NonUniqueEnums) {
|
||||
flake8_pie::rules::non_unique_enums(checker, stmt, body);
|
||||
@@ -1142,7 +1139,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pygrep_hooks::rules::non_existent_mock_method(checker, test);
|
||||
}
|
||||
}
|
||||
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
|
||||
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
|
||||
if checker.enabled(Rule::AssertRaisesException) {
|
||||
flake8_bugbear::rules::assert_raises_exception(checker, items);
|
||||
}
|
||||
@@ -1152,7 +1149,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::MultipleWithStatements) {
|
||||
flake8_simplify::rules::multiple_with_statements(
|
||||
checker,
|
||||
with_,
|
||||
with_stmt,
|
||||
checker.semantic.current_statement_parent(),
|
||||
);
|
||||
}
|
||||
@@ -1171,15 +1168,21 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
perflint::rules::try_except_in_loop(checker, body);
|
||||
}
|
||||
}
|
||||
Stmt::For(ast::StmtFor {
|
||||
target,
|
||||
body,
|
||||
iter,
|
||||
orelse,
|
||||
..
|
||||
}) => {
|
||||
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
|
||||
{
|
||||
Stmt::For(
|
||||
for_stmt @ ast::StmtFor {
|
||||
target,
|
||||
body,
|
||||
iter,
|
||||
orelse,
|
||||
is_async,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
if checker.any_enabled(&[
|
||||
Rule::UnusedLoopControlVariable,
|
||||
Rule::IncorrectDictIterator,
|
||||
Rule::YieldInForLoop,
|
||||
]) {
|
||||
checker.deferred.for_loops.push(checker.semantic.snapshot());
|
||||
}
|
||||
if checker.enabled(Rule::LoopVariableOverridesIterator) {
|
||||
@@ -1200,17 +1203,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::IterationOverSet) {
|
||||
pylint::rules::iteration_over_set(checker, iter);
|
||||
}
|
||||
if stmt.is_for_stmt() {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_for(checker, target, iter);
|
||||
}
|
||||
if checker.enabled(Rule::TryExceptInLoop) {
|
||||
perflint::rules::try_except_in_loop(checker, body);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::ManualListComprehension) {
|
||||
perflint::rules::manual_list_comprehension(checker, target, body);
|
||||
}
|
||||
@@ -1220,6 +1212,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryListCast) {
|
||||
perflint::rules::unnecessary_list_cast(checker, iter);
|
||||
}
|
||||
if !is_async {
|
||||
if checker.enabled(Rule::ReimplementedBuiltin) {
|
||||
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
|
||||
}
|
||||
if checker.enabled(Rule::InDictKeys) {
|
||||
flake8_simplify::rules::key_in_dict_for(checker, for_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::TryExceptInLoop) {
|
||||
perflint::rules::try_except_in_loop(checker, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
|
||||
@@ -32,8 +32,8 @@ use itertools::Itertools;
|
||||
use log::error;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
|
||||
ExprContext, Keyword, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt,
|
||||
Suite, UnaryOp,
|
||||
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged,
|
||||
Stmt, Suite, UnaryOp,
|
||||
};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
@@ -52,7 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
|
||||
use ruff_python_semantic::analyze::{typing, visibility};
|
||||
use ruff_python_semantic::{
|
||||
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
|
||||
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
|
||||
SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_source_file::Locator;
|
||||
@@ -193,20 +194,6 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`IsolationLevel`] for fixes in the current context.
|
||||
///
|
||||
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
|
||||
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
|
||||
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
|
||||
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
|
||||
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
|
||||
parent
|
||||
.and_then(|stmt| self.semantic.statement_id(stmt))
|
||||
.map_or(IsolationLevel::default(), |node_id| {
|
||||
IsolationLevel::Group(node_id.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// The [`Locator`] for the current file, which enables extraction of source code from byte
|
||||
/// offsets.
|
||||
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
|
||||
@@ -255,6 +242,18 @@ impl<'a> Checker<'a> {
|
||||
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
|
||||
self.settings.rules.any_enabled(rules)
|
||||
}
|
||||
|
||||
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
|
||||
///
|
||||
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
|
||||
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
|
||||
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
|
||||
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
|
||||
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
|
||||
node_id
|
||||
.map(|node_id| IsolationLevel::Group(node_id.into()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for Checker<'a>
|
||||
@@ -263,7 +262,7 @@ where
|
||||
{
|
||||
fn visit_stmt(&mut self, stmt: &'b Stmt) {
|
||||
// Step 0: Pre-processing
|
||||
self.semantic.push_statement(stmt);
|
||||
self.semantic.push_node(stmt);
|
||||
|
||||
// Track whether we've seen docstrings, non-imports, etc.
|
||||
match stmt {
|
||||
@@ -619,16 +618,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the `body`, then the `handlers`, then the `orelse`, then the
|
||||
// `finalbody`, but treat the body and the `orelse` as a single branch for
|
||||
// flow analysis purposes.
|
||||
let branch = self.semantic.push_branch();
|
||||
self.semantic.handled_exceptions.push(handled_exceptions);
|
||||
self.visit_body(body);
|
||||
self.semantic.handled_exceptions.pop();
|
||||
self.semantic.pop_branch();
|
||||
|
||||
for except_handler in handlers {
|
||||
self.semantic.push_branch();
|
||||
self.visit_except_handler(except_handler);
|
||||
self.semantic.pop_branch();
|
||||
}
|
||||
|
||||
self.semantic.set_branch(branch);
|
||||
self.visit_body(orelse);
|
||||
self.semantic.pop_branch();
|
||||
|
||||
self.semantic.push_branch();
|
||||
self.visit_body(finalbody);
|
||||
self.semantic.pop_branch();
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target,
|
||||
@@ -708,6 +719,7 @@ where
|
||||
) => {
|
||||
self.visit_boolean_test(test);
|
||||
|
||||
self.semantic.push_branch();
|
||||
if typing::is_type_checking_block(stmt_if, &self.semantic) {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_type_checking_block(stmt);
|
||||
@@ -716,9 +728,12 @@ where
|
||||
} else {
|
||||
self.visit_body(body);
|
||||
}
|
||||
self.semantic.pop_branch();
|
||||
|
||||
for clause in elif_else_clauses {
|
||||
self.semantic.push_branch();
|
||||
self.visit_elif_else_clause(clause);
|
||||
self.semantic.pop_branch();
|
||||
}
|
||||
}
|
||||
_ => visitor::walk_stmt(self, stmt),
|
||||
@@ -759,7 +774,7 @@ where
|
||||
analyze::statement(stmt, self);
|
||||
|
||||
self.semantic.flags = flags_snapshot;
|
||||
self.semantic.pop_statement();
|
||||
self.semantic.pop_node();
|
||||
}
|
||||
|
||||
fn visit_annotation(&mut self, expr: &'b Expr) {
|
||||
@@ -795,7 +810,7 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.semantic.push_expression(expr);
|
||||
self.semantic.push_node(expr);
|
||||
|
||||
// Store the flags prior to any further descent, so that we can restore them after visiting
|
||||
// the node.
|
||||
@@ -874,18 +889,20 @@ where
|
||||
},
|
||||
) => {
|
||||
// Visit the default arguments, but avoid the body, which will be deferred.
|
||||
for ParameterWithDefault {
|
||||
default,
|
||||
parameter: _,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
if let Some(expr) = &default {
|
||||
self.visit_expr(expr);
|
||||
if let Some(parameters) = parameters {
|
||||
for ParameterWithDefault {
|
||||
default,
|
||||
parameter: _,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
if let Some(expr) = &default {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,7 +1230,7 @@ where
|
||||
analyze::expression(expr, self);
|
||||
|
||||
self.semantic.flags = flags_snapshot;
|
||||
self.semantic.pop_expression();
|
||||
self.semantic.pop_node();
|
||||
}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
|
||||
@@ -1351,6 +1368,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
|
||||
self.visit_pattern(&match_case.pattern);
|
||||
if let Some(expr) = &match_case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
|
||||
self.semantic.push_branch();
|
||||
self.visit_body(&match_case.body);
|
||||
self.semantic.pop_branch();
|
||||
}
|
||||
|
||||
fn visit_type_param(&mut self, type_param: &'b ast::TypeParam) {
|
||||
// Step 1: Binding
|
||||
match type_param {
|
||||
@@ -1834,7 +1862,9 @@ impl<'a> Checker<'a> {
|
||||
range: _,
|
||||
}) = expr
|
||||
{
|
||||
self.visit_parameters(parameters);
|
||||
if let Some(parameters) = parameters {
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.visit_expr(body);
|
||||
} else {
|
||||
unreachable!("Expected Expr::Lambda");
|
||||
@@ -1855,7 +1885,7 @@ impl<'a> Checker<'a> {
|
||||
.map(|binding_id| &self.semantic.bindings[binding_id])
|
||||
.filter_map(|binding| match &binding.kind {
|
||||
BindingKind::Export(Export { names }) => {
|
||||
Some(names.iter().map(|name| (*name, binding.range)))
|
||||
Some(names.iter().map(|name| (*name, binding.range())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
use ruff_python_ast::Ranged;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{
|
||||
|
||||
@@ -110,8 +110,8 @@ pub(crate) fn check_noqa(
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
|
||||
diagnostic
|
||||
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
|
||||
);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
if valid_codes.is_empty() {
|
||||
#[allow(deprecated)]
|
||||
diagnostic
|
||||
.set_fix_from_edit(delete_noqa(directive.range(), locator));
|
||||
diagnostic.set_fix(Fix::automatic(delete_noqa(
|
||||
directive.range(),
|
||||
locator,
|
||||
)));
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
directive.range(),
|
||||
)));
|
||||
|
||||
@@ -216,6 +216,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
|
||||
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
|
||||
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
|
||||
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
||||
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
|
||||
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
|
||||
@@ -707,6 +708,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Pie, "800") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessarySpread),
|
||||
(Flake8Pie, "804") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryDictKwargs),
|
||||
(Flake8Pie, "807") => (RuleGroup::Unspecified, rules::flake8_pie::rules::ReimplementedListBuiltin),
|
||||
(Flake8Pie, "808") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryRangeStart),
|
||||
(Flake8Pie, "810") => (RuleGroup::Unspecified, rules::flake8_pie::rules::MultipleStartsEndsWith),
|
||||
|
||||
// flake8-commas
|
||||
@@ -816,6 +818,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
|
||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{
|
||||
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
|
||||
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
|
||||
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
|
||||
};
|
||||
use ruff_python_codegen::Stylist;
|
||||
|
||||
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
match libcst_native::parse_module(module_text, None) {
|
||||
@@ -12,13 +14,6 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract expression from source"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
|
||||
match libcst_native::parse_statement(statement_text) {
|
||||
Ok(statement) => Ok(statement),
|
||||
@@ -205,3 +200,59 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
|
||||
bail!("Expected Statement::Compound")
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the source code for an expression, return the parsed [`Expression`].
|
||||
///
|
||||
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
|
||||
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
|
||||
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract expression from source"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a transformation function over an expression.
|
||||
///
|
||||
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
|
||||
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
|
||||
/// source). This method instead wraps the expression in "fake" parentheses, runs the
|
||||
/// transformation, then removes the "fake" parentheses.
|
||||
pub(crate) fn transform_expression(
|
||||
source_code: &str,
|
||||
stylist: &Stylist,
|
||||
func: impl FnOnce(Expression) -> Result<Expression>,
|
||||
) -> Result<String> {
|
||||
// Wrap the expression in parentheses.
|
||||
let source_code = format!("({source_code})");
|
||||
let expression = match_expression(&source_code)?;
|
||||
|
||||
// Run the function on the expression.
|
||||
let expression = func(expression)?;
|
||||
|
||||
// Codegen the expression.
|
||||
let mut source_code = expression.codegen_stylist(stylist);
|
||||
|
||||
// Drop the outer parentheses.
|
||||
source_code.drain(0..1);
|
||||
source_code.drain(source_code.len() - 1..source_code.len());
|
||||
Ok(source_code)
|
||||
}
|
||||
|
||||
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
|
||||
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
|
||||
pub(crate) fn transform_expression_text(
|
||||
source_code: &str,
|
||||
func: impl FnOnce(String) -> Result<String>,
|
||||
) -> Result<String> {
|
||||
// Wrap the expression in parentheses.
|
||||
let source_code = format!("({source_code})");
|
||||
|
||||
// Run the function on the expression.
|
||||
let mut transformed = func(source_code)?;
|
||||
|
||||
// Drop the outer parentheses.
|
||||
transformed.drain(0..1);
|
||||
transformed.drain(transformed.len() - 1..transformed.len());
|
||||
Ok(transformed)
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ y = 2
|
||||
z = x + 1";
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22)),])
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22))])
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::fmt::{Debug, Formatter};
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_ast::{Expr, Ranged};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_python_semantic::Definition;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
pub(crate) mod extraction;
|
||||
pub(crate) mod google;
|
||||
@@ -28,43 +27,34 @@ impl<'a> Docstring<'a> {
|
||||
DocstringBody { docstring: self }
|
||||
}
|
||||
|
||||
pub(crate) fn start(&self) -> TextSize {
|
||||
self.expr.start()
|
||||
}
|
||||
|
||||
pub(crate) fn end(&self) -> TextSize {
|
||||
self.expr.end()
|
||||
}
|
||||
|
||||
pub(crate) fn range(&self) -> TextRange {
|
||||
self.expr.range()
|
||||
}
|
||||
|
||||
pub(crate) fn leading_quote(&self) -> &'a str {
|
||||
&self.contents[TextRange::up_to(self.body_range.start())]
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for Docstring<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.expr.range()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct DocstringBody<'a> {
|
||||
docstring: &'a Docstring<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DocstringBody<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn start(self) -> TextSize {
|
||||
self.range().start()
|
||||
}
|
||||
|
||||
pub(crate) fn range(self) -> TextRange {
|
||||
self.docstring.body_range + self.docstring.start()
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(self) -> &'a str {
|
||||
&self.docstring.contents[self.docstring.body_range]
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for DocstringBody<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.docstring.body_range + self.docstring.start()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DocstringBody<'_> {
|
||||
type Target = str;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ruff_python_ast::docstrings::{leading_space, leading_words};
|
||||
use ruff_python_ast::Ranged;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
@@ -366,6 +367,12 @@ impl<'a> SectionContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for SectionContext<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SectionContext<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SectionContext")
|
||||
|
||||
@@ -194,7 +194,7 @@ impl<'a> Importer<'a> {
|
||||
// import and the current location, and thus the symbol would not be available). It's also
|
||||
// unclear whether should add an import statement at the start of the file, since it could
|
||||
// be shadowed between the import and the current location.
|
||||
if imported_name.range().start() > at {
|
||||
if imported_name.start() > at {
|
||||
return Some(Err(ResolutionError::ImportAfterUsage));
|
||||
}
|
||||
|
||||
@@ -301,12 +301,14 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
if let Stmt::ImportFrom(ast::StmtImportFrom {
|
||||
module: name,
|
||||
names,
|
||||
level,
|
||||
..
|
||||
range: _,
|
||||
}) = stmt
|
||||
{
|
||||
if level.map_or(true, |level| level.to_u32() == 0)
|
||||
&& name.as_ref().is_some_and(|name| name == module)
|
||||
&& names.iter().all(|alias| alias.name.as_str() != "*")
|
||||
{
|
||||
import_from = Some(*stmt);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
/// When we lint a jupyter notebook, we have to translate the row/column based on
|
||||
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct JupyterIndex {
|
||||
pub struct NotebookIndex {
|
||||
/// Enter a row (1-based), get back the cell (1-based)
|
||||
pub(super) row_to_cell: Vec<u32>,
|
||||
/// Enter a row (1-based), get back the row in cell (1-based)
|
||||
pub(super) row_to_row_in_cell: Vec<u32>,
|
||||
}
|
||||
|
||||
impl JupyterIndex {
|
||||
impl NotebookIndex {
|
||||
/// Returns the cell number (1-based) for the given row (1-based).
|
||||
pub fn cell(&self, row: usize) -> Option<u32> {
|
||||
self.row_to_cell.get(row).copied()
|
||||
|
||||
@@ -9,6 +9,7 @@ use itertools::Itertools;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
use uuid::Uuid;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_parser::lexer::lex;
|
||||
@@ -17,7 +18,7 @@ use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::autofix::source_map::{SourceMap, SourceMarker};
|
||||
use crate::jupyter::index::JupyterIndex;
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
|
||||
use crate::rules::pycodestyle::rules::SyntaxError;
|
||||
use crate::IOError;
|
||||
@@ -33,7 +34,7 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
err
|
||||
)
|
||||
})?;
|
||||
let code = notebook.content().to_string();
|
||||
let code = notebook.source_code().to_string();
|
||||
notebook.update_cell_content(&code);
|
||||
let mut writer = Vec::new();
|
||||
notebook.write_inner(&mut writer)?;
|
||||
@@ -82,8 +83,8 @@ impl Cell {
|
||||
Cell::Code(cell) => &cell.source,
|
||||
_ => return false,
|
||||
};
|
||||
// Ignore cells containing cell magic. This is different from line magic
|
||||
// which is allowed and ignored by the parser.
|
||||
// Ignore cells containing cell magic as they act on the entire cell
|
||||
// as compared to line magic which acts on a single line.
|
||||
!match source {
|
||||
SourceValue::String(string) => string
|
||||
.lines()
|
||||
@@ -103,10 +104,10 @@ pub struct Notebook {
|
||||
/// separated by a newline and a trailing newline. The trailing newline
|
||||
/// is added to make sure that each cell ends with a newline which will
|
||||
/// be removed when updating the cell content.
|
||||
content: String,
|
||||
source_code: String,
|
||||
/// The index of the notebook. This is used to map between the concatenated
|
||||
/// source code and the original notebook.
|
||||
index: OnceCell<JupyterIndex>,
|
||||
index: OnceCell<NotebookIndex>,
|
||||
/// The raw notebook i.e., the deserialized version of JSON string.
|
||||
raw: RawNotebook,
|
||||
/// The offsets of each cell in the concatenated source code. This includes
|
||||
@@ -132,8 +133,8 @@ impl Notebook {
|
||||
}
|
||||
|
||||
/// Read the Jupyter Notebook from its JSON string.
|
||||
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(Cursor::new(contents))
|
||||
pub fn from_source_code(source_code: &str) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(Cursor::new(source_code))
|
||||
}
|
||||
|
||||
/// Read a Jupyter Notebook from a [`Read`] implementor.
|
||||
@@ -156,7 +157,7 @@ impl Notebook {
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
Ok(notebook) => notebook,
|
||||
Err(err) => {
|
||||
// Translate the error into a diagnostic
|
||||
@@ -262,13 +263,30 @@ impl Notebook {
|
||||
cell_offsets.push(current_offset);
|
||||
}
|
||||
|
||||
// Add cell ids to 4.5+ notebooks if they are missing
|
||||
// https://github.com/astral-sh/ruff/issues/6834
|
||||
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#required-field
|
||||
if raw_notebook.nbformat == 4 && raw_notebook.nbformat_minor >= 5 {
|
||||
for cell in &mut raw_notebook.cells {
|
||||
let id = match cell {
|
||||
Cell::Code(cell) => &mut cell.id,
|
||||
Cell::Markdown(cell) => &mut cell.id,
|
||||
Cell::Raw(cell) => &mut cell.id,
|
||||
};
|
||||
if id.is_none() {
|
||||
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#questions
|
||||
*id = Some(Uuid::new_v4().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
raw: raw_notebook,
|
||||
index: OnceCell::new(),
|
||||
// The additional newline at the end is to maintain consistency for
|
||||
// all cells. These newlines will be removed before updating the
|
||||
// source code with the transformed content. Refer `update_cell_content`.
|
||||
content: contents.join("\n") + "\n",
|
||||
source_code: contents.join("\n") + "\n",
|
||||
cell_offsets,
|
||||
valid_code_cells,
|
||||
trailing_newline,
|
||||
@@ -368,7 +386,7 @@ impl Notebook {
|
||||
///
|
||||
/// The index building is expensive as it needs to go through the content of
|
||||
/// every valid code cell.
|
||||
fn build_index(&self) -> JupyterIndex {
|
||||
fn build_index(&self) -> NotebookIndex {
|
||||
let mut row_to_cell = vec![0];
|
||||
let mut row_to_row_in_cell = vec![0];
|
||||
|
||||
@@ -395,7 +413,7 @@ impl Notebook {
|
||||
row_to_row_in_cell.extend(1..=line_count);
|
||||
}
|
||||
|
||||
JupyterIndex {
|
||||
NotebookIndex {
|
||||
row_to_cell,
|
||||
row_to_row_in_cell,
|
||||
}
|
||||
@@ -404,8 +422,8 @@ impl Notebook {
|
||||
/// Return the notebook content.
|
||||
///
|
||||
/// This is the concatenation of all Python code cells.
|
||||
pub(crate) fn content(&self) -> &str {
|
||||
&self.content
|
||||
pub fn source_code(&self) -> &str {
|
||||
&self.source_code
|
||||
}
|
||||
|
||||
/// Return the Jupyter notebook index.
|
||||
@@ -413,7 +431,7 @@ impl Notebook {
|
||||
/// The index is built only once when required. This is only used to
|
||||
/// report diagnostics, so by that time all of the autofixes must have
|
||||
/// been applied if `--fix` was passed.
|
||||
pub(crate) fn index(&self) -> &JupyterIndex {
|
||||
pub(crate) fn index(&self) -> &NotebookIndex {
|
||||
self.index.get_or_init(|| self.build_index())
|
||||
}
|
||||
|
||||
@@ -424,12 +442,13 @@ impl Notebook {
|
||||
}
|
||||
|
||||
/// Update the notebook with the given sourcemap and transformed content.
|
||||
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: &str) {
|
||||
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: String) {
|
||||
// Cell offsets must be updated before updating the cell content as
|
||||
// it depends on the offsets to extract the cell content.
|
||||
self.index.take();
|
||||
self.update_cell_offsets(source_map);
|
||||
self.update_cell_content(transformed);
|
||||
self.content = transformed.to_string();
|
||||
self.update_cell_content(&transformed);
|
||||
self.source_code = transformed;
|
||||
}
|
||||
|
||||
/// Return a slice of [`Cell`] in the Jupyter notebook.
|
||||
@@ -472,18 +491,22 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::jupyter::index::JupyterIndex;
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::Cell;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::registry::Rule;
|
||||
use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path};
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{
|
||||
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
|
||||
TestedNotebook,
|
||||
};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = test_resource_path("fixtures/jupyter/cell").join(path);
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&contents)?)
|
||||
let source_code = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -536,7 +559,7 @@ mod tests {
|
||||
fn test_concat_notebook() -> Result<()> {
|
||||
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
|
||||
assert_eq!(
|
||||
notebook.content,
|
||||
notebook.source_code,
|
||||
r#"def unused_variable():
|
||||
x = 1
|
||||
y = 2
|
||||
@@ -556,7 +579,7 @@ print("after empty cells")
|
||||
);
|
||||
assert_eq!(
|
||||
notebook.index(),
|
||||
&JupyterIndex {
|
||||
&NotebookIndex {
|
||||
row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8],
|
||||
row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1],
|
||||
}
|
||||
@@ -578,49 +601,64 @@ print("after empty cells")
|
||||
#[test]
|
||||
fn test_import_sorting() -> Result<()> {
|
||||
let path = "isort.ipynb".to_string();
|
||||
let (diagnostics, source_kind, _) = test_notebook_path(
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("isort_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnsortedImports),
|
||||
)?;
|
||||
assert_messages!(diagnostics, path, source_kind);
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipy_escape_command() -> Result<()> {
|
||||
let path = "ipy_escape_command.ipynb".to_string();
|
||||
let (diagnostics, source_kind, _) = test_notebook_path(
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("ipy_escape_command_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
assert_messages!(diagnostics, path, source_kind);
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_variable() -> Result<()> {
|
||||
let path = "unused_variable.ipynb".to_string();
|
||||
let (diagnostics, source_kind, _) = test_notebook_path(
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("unused_variable_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedVariable),
|
||||
)?;
|
||||
assert_messages!(diagnostics, path, source_kind);
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let path = "before_fix.ipynb".to_string();
|
||||
let (_, _, source_kind) = test_notebook_path(
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
path,
|
||||
Path::new("after_fix.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
let mut writer = Vec::new();
|
||||
source_kind.expect_jupyter().write_inner(&mut writer)?;
|
||||
fixed_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
let expected =
|
||||
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;
|
||||
@@ -641,4 +679,28 @@ print("after empty cells")
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Version <4.5, don't emit cell ids
|
||||
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
|
||||
// Version 4.5, cell ids are missing and need to be added
|
||||
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
|
||||
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
|
||||
let source_notebook = read_jupyter_notebook(path)?;
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (_, transformed) = test_contents(
|
||||
&source_kind,
|
||||
path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
);
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
let mut writer = Vec::new();
|
||||
linted_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
if has_id {
|
||||
assert!(actual.contains(r#""id": ""#));
|
||||
} else {
|
||||
assert!(!actual.contains(r#""id":"#));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ pub struct CodeCell {
|
||||
/// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but
|
||||
/// it's required in v4.5. Main issue is that pycharm creates notebooks without an id
|
||||
/// <https://youtrack.jetbrains.com/issue/PY-59438/Jupyter-notebooks-created-with-PyCharm-are-missing-the-id-field-in-cells-in-the-.ipynb-json>
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
/// Cell-level metadata.
|
||||
pub metadata: Value,
|
||||
|
||||
@@ -63,7 +63,7 @@ pub struct FixerResult<'a> {
|
||||
/// The result returned by the linter, after applying any fixes.
|
||||
pub result: LinterResult<(Vec<Message>, Option<ImportMap>)>,
|
||||
/// The resulting source code, after applying any fixes.
|
||||
pub transformed: Cow<'a, str>,
|
||||
pub transformed: Cow<'a, SourceKind>,
|
||||
/// The number of fixes applied for each [`Rule`].
|
||||
pub fixed: FixTable,
|
||||
}
|
||||
@@ -147,7 +147,7 @@ pub fn check_path(
|
||||
match ruff_python_parser::parse_program_tokens(
|
||||
tokens,
|
||||
&path.to_string_lossy(),
|
||||
source_type.is_jupyter(),
|
||||
source_type.is_ipynb(),
|
||||
) {
|
||||
Ok(python_ast) => {
|
||||
if use_ast {
|
||||
@@ -335,19 +335,19 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
|
||||
/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source
|
||||
/// code.
|
||||
pub fn lint_only(
|
||||
contents: &str,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
noqa: flags::Noqa,
|
||||
source_kind: Option<&SourceKind>,
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
|
||||
let tokens: Vec<LexResult> =
|
||||
ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(contents);
|
||||
let locator = Locator::new(source_kind.source_code());
|
||||
|
||||
// Detect the current code style (lazily).
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
@@ -374,7 +374,7 @@ pub fn lint_only(
|
||||
&directives,
|
||||
settings,
|
||||
noqa,
|
||||
source_kind,
|
||||
Some(source_kind),
|
||||
source_type,
|
||||
);
|
||||
|
||||
@@ -416,15 +416,14 @@ fn diagnostics_to_messages(
|
||||
/// Generate `Diagnostic`s from source code content, iteratively autofixing
|
||||
/// until stable.
|
||||
pub fn lint_fix<'a>(
|
||||
contents: &'a str,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
noqa: flags::Noqa,
|
||||
settings: &Settings,
|
||||
source_kind: &mut SourceKind,
|
||||
source_kind: &'a SourceKind,
|
||||
source_type: PySourceType,
|
||||
) -> Result<FixerResult<'a>> {
|
||||
let mut transformed = Cow::Borrowed(contents);
|
||||
let mut transformed = Cow::Borrowed(source_kind);
|
||||
|
||||
// Track the number of fixed errors across iterations.
|
||||
let mut fixed = FxHashMap::default();
|
||||
@@ -439,10 +438,10 @@ pub fn lint_fix<'a>(
|
||||
loop {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> =
|
||||
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
|
||||
ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(&transformed);
|
||||
let locator = Locator::new(transformed.source_code());
|
||||
|
||||
// Detect the current code style (lazily).
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
@@ -482,7 +481,7 @@ pub fn lint_fix<'a>(
|
||||
if parseable && result.error.is_some() {
|
||||
report_autofix_syntax_error(
|
||||
path,
|
||||
&transformed,
|
||||
transformed.source_code(),
|
||||
&result.error.unwrap(),
|
||||
fixed.keys().copied(),
|
||||
);
|
||||
@@ -503,12 +502,7 @@ pub fn lint_fix<'a>(
|
||||
*fixed.entry(rule).or_default() += count;
|
||||
}
|
||||
|
||||
if let SourceKind::Jupyter(notebook) = source_kind {
|
||||
notebook.update(&source_map, &fixed_contents);
|
||||
}
|
||||
|
||||
// Store the fixed contents.
|
||||
transformed = Cow::Owned(fixed_contents);
|
||||
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
|
||||
|
||||
// Increment the iteration count.
|
||||
iterations += 1;
|
||||
@@ -517,7 +511,7 @@ pub fn lint_fix<'a>(
|
||||
continue;
|
||||
}
|
||||
|
||||
report_failed_to_converge_error(path, &transformed, &result.data.0);
|
||||
report_failed_to_converge_error(path, transformed.source_code(), &result.data.0);
|
||||
}
|
||||
|
||||
return Ok(FixerResult {
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Emitter for AzureEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let location = if context.is_jupyter_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
SourceLocation::default()
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Emitter for GithubEmitter {
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let source_location = message.compute_start_location();
|
||||
let location = if context.is_jupyter_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
SourceLocation::default()
|
||||
|
||||
@@ -63,7 +63,7 @@ impl Serialize for SerializedMessages<'_> {
|
||||
let start_location = message.compute_start_location();
|
||||
let end_location = message.compute_end_location();
|
||||
|
||||
let lines = if self.context.is_jupyter_notebook(message.filename()) {
|
||||
let lines = if self.context.is_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
json!({
|
||||
|
||||
@@ -7,13 +7,12 @@ use colored::Colorize;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{JupyterIndex, Notebook};
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::message::diff::calculate_print_width;
|
||||
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{
|
||||
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
|
||||
};
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GroupedEmitter {
|
||||
@@ -66,10 +65,7 @@ impl Emitter for GroupedEmitter {
|
||||
writer,
|
||||
"{}",
|
||||
DisplayGroupedMessage {
|
||||
jupyter_index: context
|
||||
.source_kind(message.filename())
|
||||
.and_then(SourceKind::notebook)
|
||||
.map(Notebook::index),
|
||||
jupyter_index: context.notebook(message.filename()).map(Notebook::index),
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
show_source: self.show_source,
|
||||
@@ -96,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
|
||||
show_source: bool,
|
||||
row_length: NonZeroUsize,
|
||||
column_length: NonZeroUsize,
|
||||
jupyter_index: Option<&'a JupyterIndex>,
|
||||
jupyter_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Display for DisplayGroupedMessage<'_> {
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Emitter for JunitEmitter {
|
||||
} = message;
|
||||
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
|
||||
status.set_message(message.kind.body.clone());
|
||||
let location = if context.is_jupyter_notebook(message.filename()) {
|
||||
let location = if context.is_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
SourceLocation::default()
|
||||
|
||||
@@ -3,10 +3,8 @@ use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::source_kind::SourceKind;
|
||||
pub use azure::AzureEmitter;
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
@@ -17,8 +15,11 @@ pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_source_file::{SourceFile, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
pub use text::TextEmitter;
|
||||
|
||||
use crate::jupyter::Notebook;
|
||||
|
||||
mod azure;
|
||||
mod diff;
|
||||
mod github;
|
||||
@@ -129,33 +130,31 @@ pub trait Emitter {
|
||||
|
||||
/// Context passed to [`Emitter`].
|
||||
pub struct EmitterContext<'a> {
|
||||
source_kind: &'a FxHashMap<String, SourceKind>,
|
||||
notebooks: &'a FxHashMap<String, Notebook>,
|
||||
}
|
||||
|
||||
impl<'a> EmitterContext<'a> {
|
||||
pub fn new(source_kind: &'a FxHashMap<String, SourceKind>) -> Self {
|
||||
Self { source_kind }
|
||||
pub fn new(notebooks: &'a FxHashMap<String, Notebook>) -> Self {
|
||||
Self { notebooks }
|
||||
}
|
||||
|
||||
/// Tests if the file with `name` is a jupyter notebook.
|
||||
pub fn is_jupyter_notebook(&self, name: &str) -> bool {
|
||||
self.source_kind
|
||||
.get(name)
|
||||
.is_some_and(SourceKind::is_jupyter)
|
||||
pub fn is_notebook(&self, name: &str) -> bool {
|
||||
self.notebooks.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn source_kind(&self, name: &str) -> Option<&SourceKind> {
|
||||
self.source_kind.get(name)
|
||||
pub fn notebook(&self, name: &str) -> Option<&Notebook> {
|
||||
self.notebooks.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Emitter for PylintEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in messages {
|
||||
let row = if context.is_jupyter_notebook(message.filename()) {
|
||||
let row = if context.is_notebook(message.filename()) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
OneIndexed::from_zero_indexed(0)
|
||||
|
||||
@@ -6,17 +6,16 @@ use annotate_snippets::display_list::{DisplayList, FormatOptions};
|
||||
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_source_file::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{JupyterIndex, Notebook};
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::line_width::{LineWidth, TabSize};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
use crate::registry::AsRule;
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
@@ -72,10 +71,7 @@ impl Emitter for TextEmitter {
|
||||
)?;
|
||||
|
||||
let start_location = message.compute_start_location();
|
||||
let jupyter_index = context
|
||||
.source_kind(message.filename())
|
||||
.and_then(SourceKind::notebook)
|
||||
.map(Notebook::index);
|
||||
let jupyter_index = context.notebook(message.filename()).map(Notebook::index);
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {
|
||||
@@ -165,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
|
||||
|
||||
pub(super) struct MessageCodeFrame<'a> {
|
||||
pub(crate) message: &'a Message,
|
||||
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
|
||||
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
|
||||
@@ -562,7 +562,7 @@ fn add_noqa_inner(
|
||||
let mut prev_end = TextSize::default();
|
||||
|
||||
for (offset, (rules, directive)) in matches_by_line {
|
||||
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
|
||||
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
|
||||
|
||||
let line = locator.full_line(offset);
|
||||
|
||||
@@ -619,7 +619,7 @@ fn add_noqa_inner(
|
||||
prev_end = offset + line.text_len();
|
||||
}
|
||||
|
||||
output.push_str(&locator.contents()[usize::from(prev_end)..]);
|
||||
output.push_str(locator.after(prev_end));
|
||||
|
||||
(count, output)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::Ranged;
|
||||
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
|
||||
|
||||
pub(crate) struct Renamer;
|
||||
@@ -220,12 +221,12 @@ impl Renamer {
|
||||
BindingKind::Import(_) | BindingKind::FromImport(_) => {
|
||||
if binding.is_alias() {
|
||||
// Ex) Rename `import pandas as alias` to `import pandas as pd`.
|
||||
Some(Edit::range_replacement(target.to_string(), binding.range))
|
||||
Some(Edit::range_replacement(target.to_string(), binding.range()))
|
||||
} else {
|
||||
// Ex) Rename `import pandas` to `import pandas as pd`.
|
||||
Some(Edit::range_replacement(
|
||||
format!("{name} as {target}"),
|
||||
binding.range,
|
||||
binding.range(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -234,7 +235,7 @@ impl Renamer {
|
||||
let module_name = import.call_path.first().unwrap();
|
||||
Some(Edit::range_replacement(
|
||||
format!("{module_name} as {target}"),
|
||||
binding.range,
|
||||
binding.range(),
|
||||
))
|
||||
}
|
||||
// Avoid renaming builtins and other "special" bindings.
|
||||
@@ -254,7 +255,7 @@ impl Renamer {
|
||||
| BindingKind::FunctionDefinition(_)
|
||||
| BindingKind::Deletion
|
||||
| BindingKind::UnboundException(_) => {
|
||||
Some(Edit::range_replacement(target.to_string(), binding.range))
|
||||
Some(Edit::range_replacement(target.to_string(), binding.range()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
|
||||
static MULTILINE_ASSIGNMENT_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^\s*([(\[]\s*)?(\w+\s*,\s*)*\w+\s*([)\]]\s*)?=.*[(\[{]$").unwrap());
|
||||
static PARTIAL_DICTIONARY_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*$"#).unwrap());
|
||||
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#).unwrap());
|
||||
static PRINT_RETURN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(print|return)\b\s*").unwrap());
|
||||
|
||||
/// Returns `true` if a comment contains Python code.
|
||||
|
||||
@@ -105,5 +105,47 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
|
||||
19 19 | class A():
|
||||
20 20 | pass
|
||||
21 |- # b = c
|
||||
22 21 |
|
||||
23 22 |
|
||||
24 23 | dictionary = {
|
||||
|
||||
ERA001.py:26:5: ERA001 [*] Found commented-out code
|
||||
|
|
||||
24 | dictionary = {
|
||||
25 | # "key1": 123, # noqa: ERA001
|
||||
26 | # "key2": 456,
|
||||
| ^^^^^^^^^^^^^^ ERA001
|
||||
27 | # "key3": 789, # test
|
||||
28 | }
|
||||
|
|
||||
= help: Remove commented-out code
|
||||
|
||||
ℹ Possible fix
|
||||
23 23 |
|
||||
24 24 | dictionary = {
|
||||
25 25 | # "key1": 123, # noqa: ERA001
|
||||
26 |- # "key2": 456,
|
||||
27 26 | # "key3": 789, # test
|
||||
28 27 | }
|
||||
29 28 |
|
||||
|
||||
ERA001.py:27:5: ERA001 [*] Found commented-out code
|
||||
|
|
||||
25 | # "key1": 123, # noqa: ERA001
|
||||
26 | # "key2": 456,
|
||||
27 | # "key3": 789, # test
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
|
||||
28 | }
|
||||
|
|
||||
= help: Remove commented-out code
|
||||
|
||||
ℹ Possible fix
|
||||
24 24 | dictionary = {
|
||||
25 25 | # "key1": 123, # noqa: ERA001
|
||||
26 26 | # "key2": 456,
|
||||
27 |- # "key3": 789, # test
|
||||
28 27 | }
|
||||
29 28 |
|
||||
30 29 | #import os # noqa
|
||||
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
use ruff_python_ast::{PySourceType, Ranged};
|
||||
use ruff_python_parser::{lexer, AsMode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
/// ANN204
|
||||
pub(crate) fn add_return_annotation<T: Ranged>(
|
||||
statement: &T,
|
||||
annotation: &str,
|
||||
source_type: PySourceType,
|
||||
locator: &Locator,
|
||||
) -> Result<Edit> {
|
||||
let contents = &locator.contents()[statement.range()];
|
||||
|
||||
// Find the colon (following the `def` keyword).
|
||||
let mut seen_lpar = false;
|
||||
let mut seen_rpar = false;
|
||||
let mut count = 0u32;
|
||||
for (tok, range) in
|
||||
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
|
||||
{
|
||||
if seen_lpar && seen_rpar {
|
||||
if matches!(tok, Tok::Colon) {
|
||||
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
if count == 0 {
|
||||
seen_lpar = true;
|
||||
}
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
if matches!(tok, Tok::Rpar) {
|
||||
count = count.saturating_sub(1);
|
||||
if count == 0 {
|
||||
seen_rpar = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Unable to locate colon in function definition");
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Rules from [flake8-annotations](https://pypi.org/project/flake8-annotations/).
|
||||
mod fixes;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::ReturnStatementVisitor;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
@@ -11,7 +11,6 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_annotations::fixes;
|
||||
use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
|
||||
/// ## What it does
|
||||
@@ -704,15 +703,10 @@ pub(crate) fn definition(
|
||||
function.identifier(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::add_return_annotation(
|
||||
function,
|
||||
"None",
|
||||
checker.source_type,
|
||||
checker.locator(),
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
diagnostic.set_fix(Fix::suggested(Edit::insertion(
|
||||
" -> None".to_string(),
|
||||
function.parameters.range().end(),
|
||||
)));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -727,15 +721,10 @@ 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(
|
||||
function,
|
||||
return_type,
|
||||
checker.source_type,
|
||||
checker.locator(),
|
||||
)
|
||||
.map(Fix::suggested)
|
||||
});
|
||||
diagnostic.set_fix(Fix::suggested(Edit::insertion(
|
||||
format!(" -> {return_type}"),
|
||||
function.parameters.range().end(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
|
||||
@@ -242,4 +242,22 @@ annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
||||
|
|
||||
|
||||
annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for special method `__init__`
|
||||
|
|
||||
157 | class Foo:
|
||||
158 | @decorator()
|
||||
159 | def __init__(self: "Foo", foo: int):
|
||||
| ^^^^^^^^ ANN204
|
||||
160 | ...
|
||||
|
|
||||
= help: Add `None` return type
|
||||
|
||||
ℹ Suggested fix
|
||||
156 156 |
|
||||
157 157 | class Foo:
|
||||
158 158 | @decorator()
|
||||
159 |- def __init__(self: "Foo", foo: int):
|
||||
159 |+ def __init__(self: "Foo", foo: int) -> None:
|
||||
160 160 | ...
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ use ruff_python_ast::{self as ast, Constant, Expr};
|
||||
pub(super) fn is_allowed_func_call(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"append"
|
||||
"__setattr__"
|
||||
| "append"
|
||||
| "assertEqual"
|
||||
| "assertEquals"
|
||||
| "assertNotEqual"
|
||||
@@ -26,13 +27,13 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
|
||||
| "int"
|
||||
| "is_"
|
||||
| "is_not"
|
||||
| "next"
|
||||
| "param"
|
||||
| "pop"
|
||||
| "remove"
|
||||
| "set_blocking"
|
||||
| "set_enabled"
|
||||
| "setattr"
|
||||
| "__setattr__"
|
||||
| "setdefault"
|
||||
| "str"
|
||||
)
|
||||
|
||||
@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
||||
21 | kwonly_nonvalued_nohint,
|
||||
|
|
||||
|
||||
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
|
||||
FBT.py:87:19: FBT001 Boolean-typed positional argument in function definition
|
||||
|
|
||||
85 | # FBT001: Boolean positional arg in function definition
|
||||
86 | def foo(self, value: bool) -> None:
|
||||
86 | # FBT001: Boolean positional arg in function definition
|
||||
87 | def foo(self, value: bool) -> None:
|
||||
| ^^^^^ FBT001
|
||||
87 | pass
|
||||
88 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_immutable_calls() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls".to_string();
|
||||
fn extend_immutable_calls_arg_annotation() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls_arg_annotation".to_string();
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear/B006_extended.py"),
|
||||
&Settings {
|
||||
flake8_bugbear: super::settings::Settings {
|
||||
extend_immutable_calls: vec![
|
||||
"custom.ImmutableTypeA".to_string(),
|
||||
"custom.ImmutableTypeB".to_string(),
|
||||
],
|
||||
},
|
||||
..Settings::for_rule(Rule::MutableArgumentDefault)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_immutable_calls_arg_default() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls_arg_default".to_string();
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_bugbear/B008_extended.py"),
|
||||
&Settings {
|
||||
@@ -80,6 +99,7 @@ mod tests {
|
||||
extend_immutable_calls: vec![
|
||||
"fastapi.Depends".to_string(),
|
||||
"fastapi.Query".to_string(),
|
||||
"custom.ImmutableTypeA".to_string(),
|
||||
],
|
||||
},
|
||||
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
|
||||
|
||||
@@ -90,8 +90,7 @@ impl AlwaysAutofixableViolation for DuplicateHandlerException {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let DuplicateHandlerException { names } = self;
|
||||
if names.len() == 1 {
|
||||
let name = &names[0];
|
||||
if let [name] = names.as_slice() {
|
||||
format!("Exception handler with duplicate exception: `{name}`")
|
||||
} else {
|
||||
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
|
||||
|
||||
@@ -7,7 +7,9 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func};
|
||||
use ruff_python_semantic::analyze::typing::{
|
||||
is_immutable_annotation, is_immutable_func, is_mutable_func,
|
||||
};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -20,6 +22,13 @@ use crate::checkers::ast::Checker;
|
||||
/// once, at definition time. The returned value will then be reused by all
|
||||
/// calls to the function, which can lead to unexpected behaviour.
|
||||
///
|
||||
/// Calls can be marked as an exception to this rule with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option as well.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def create_list() -> list[int]:
|
||||
@@ -60,14 +69,14 @@ impl Violation for FunctionCallInDefaultArgument {
|
||||
}
|
||||
}
|
||||
|
||||
struct ArgumentDefaultVisitor<'a> {
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
extend_immutable_calls: Vec<CallPath<'a>>,
|
||||
struct ArgumentDefaultVisitor<'a, 'b> {
|
||||
semantic: &'a SemanticModel<'b>,
|
||||
extend_immutable_calls: &'a [CallPath<'b>],
|
||||
diagnostics: Vec<(DiagnosticKind, TextRange)>,
|
||||
}
|
||||
|
||||
impl<'a> ArgumentDefaultVisitor<'a> {
|
||||
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
|
||||
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
|
||||
fn new(semantic: &'a SemanticModel<'b>, extend_immutable_calls: &'a [CallPath<'b>]) -> Self {
|
||||
Self {
|
||||
semantic,
|
||||
extend_immutable_calls,
|
||||
@@ -76,15 +85,12 @@ impl<'a> ArgumentDefaultVisitor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_expr(&mut self, expr: &'b Expr) {
|
||||
impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
match expr {
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
if !is_mutable_func(func, self.semantic)
|
||||
&& !is_immutable_func(func, self.semantic, &self.extend_immutable_calls)
|
||||
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
|
||||
{
|
||||
self.diagnostics.push((
|
||||
FunctionCallInDefaultArgument {
|
||||
@@ -114,25 +120,28 @@ pub(crate) fn function_call_in_argument_default(checker: &mut Checker, parameter
|
||||
.iter()
|
||||
.map(|target| from_qualified_name(target))
|
||||
.collect();
|
||||
let diagnostics = {
|
||||
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls);
|
||||
for ParameterWithDefault {
|
||||
default,
|
||||
parameter: _,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
if let Some(expr) = &default {
|
||||
|
||||
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
|
||||
for ParameterWithDefault {
|
||||
default,
|
||||
parameter,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
if let Some(expr) = &default {
|
||||
if !parameter.annotation.as_ref().is_some_and(|expr| {
|
||||
is_immutable_annotation(expr, checker.semantic(), &extend_immutable_calls)
|
||||
}) {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
visitor.diagnostics
|
||||
};
|
||||
for (check, range) in diagnostics {
|
||||
}
|
||||
|
||||
for (check, range) in visitor.diagnostics {
|
||||
checker.diagnostics.push(Diagnostic::new(check, range));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,10 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if parameters.includes(&loaded.id) {
|
||||
if parameters
|
||||
.as_ref()
|
||||
.is_some_and(|parameters| parameters.includes(&loaded.id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,17 +76,20 @@ where
|
||||
range: _,
|
||||
}) => {
|
||||
visitor::walk_expr(self, body);
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default: _,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
self.names.remove(parameter.name.as_str());
|
||||
|
||||
if let Some(parameters) = parameters {
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default: _,
|
||||
range: _,
|
||||
} in parameters
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
{
|
||||
self.names.remove(parameter.name.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => visitor::walk_expr(self, expr),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ast::call_path::{from_qualified_name, CallPath};
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
@@ -25,6 +26,10 @@ use crate::registry::AsRule;
|
||||
/// default, and initialize a new mutable object inside the function body
|
||||
/// for each call.
|
||||
///
|
||||
/// Arguments with immutable type annotations will be ignored by this rule.
|
||||
/// Types outside of the standard library can be marked as immutable with the
|
||||
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def add_to_list(item, some_list=[]):
|
||||
@@ -49,6 +54,9 @@ use crate::registry::AsRule;
|
||||
/// l2 = add_to_list(1) # [1]
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-bugbear.extend-immutable-calls`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
|
||||
#[violation]
|
||||
@@ -84,11 +92,18 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
|
||||
continue;
|
||||
};
|
||||
|
||||
let extend_immutable_calls: Vec<CallPath> = checker
|
||||
.settings
|
||||
.flake8_bugbear
|
||||
.extend_immutable_calls
|
||||
.iter()
|
||||
.map(|target| from_qualified_name(target))
|
||||
.collect();
|
||||
|
||||
if is_mutable_expr(default, checker.semantic())
|
||||
&& !parameter
|
||||
.annotation
|
||||
.as_ref()
|
||||
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
|
||||
&& !parameter.annotation.as_ref().is_some_and(|expr| {
|
||||
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
|
||||
})
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
|
||||
|
||||
@@ -125,7 +140,7 @@ fn move_initialization(
|
||||
let mut body = function_def.body.iter();
|
||||
|
||||
let statement = body.next()?;
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
if indexer.in_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -155,7 +170,7 @@ fn move_initialization(
|
||||
if let Some(statement) = body.next() {
|
||||
// If there's a second statement, insert _before_ it, but ensure this isn't a
|
||||
// multi-statement line.
|
||||
if indexer.preceded_by_multi_statement_line(statement, locator) {
|
||||
if indexer.in_multi_statement_line(statement, locator) {
|
||||
return None;
|
||||
}
|
||||
Edit::insertion(content, locator.line_start(statement.start()))
|
||||
|
||||
@@ -79,7 +79,6 @@ pub(crate) fn redundant_tuple_in_exception_handler(
|
||||
type_.range(),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
checker.generator().expr(elt),
|
||||
type_.range(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, Ranged};
|
||||
use ruff_python_ast::{helpers, visitor};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -105,16 +105,16 @@ where
|
||||
}
|
||||
|
||||
/// B007
|
||||
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
|
||||
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
|
||||
let control_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
finder.visit_expr(target);
|
||||
finder.visit_expr(stmt_for.target.as_ref());
|
||||
finder.names
|
||||
};
|
||||
|
||||
let used_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
for stmt in body {
|
||||
for stmt in &stmt_for.body {
|
||||
finder.visit_stmt(stmt);
|
||||
}
|
||||
finder.names
|
||||
@@ -132,9 +132,10 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
|
||||
}
|
||||
|
||||
// Avoid fixing any variables that _may_ be used, but undetectably so.
|
||||
let certainty = Certainty::from(!helpers::uses_magic_variable_access(body, |id| {
|
||||
checker.semantic().is_builtin(id)
|
||||
}));
|
||||
let certainty =
|
||||
Certainty::from(!helpers::uses_magic_variable_access(&stmt_for.body, |id| {
|
||||
checker.semantic().is_builtin(id)
|
||||
}));
|
||||
|
||||
// Attempt to rename the variable by prepending an underscore, but avoid
|
||||
// applying the fix if doing so wouldn't actually cause us to ignore the
|
||||
@@ -163,7 +164,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
|
||||
if scope
|
||||
.get_all(name)
|
||||
.map(|binding_id| checker.semantic().binding(binding_id))
|
||||
.filter(|binding| binding.range.start() >= expr.range().start())
|
||||
.filter(|binding| binding.start() >= expr.start())
|
||||
.all(|binding| !binding.is_used())
|
||||
{
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
|
||||
@@ -258,161 +258,139 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
|
||||
116 118 |
|
||||
117 119 |
|
||||
|
||||
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
233 | # B006 and B008
|
||||
234 | # We should handle arbitrary nesting of these B008.
|
||||
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
237 | # B006 and B008
|
||||
238 | # We should handle arbitrary nesting of these B008.
|
||||
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
|
||||
236 | pass
|
||||
240 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
232 232 |
|
||||
233 233 | # B006 and B008
|
||||
234 234 | # We should handle arbitrary nesting of these B008.
|
||||
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
235 |+def nested_combo(a=None):
|
||||
236 |+ if a is None:
|
||||
237 |+ a = [float(3), dt.datetime.now()]
|
||||
236 238 | pass
|
||||
237 239 |
|
||||
238 240 |
|
||||
236 236 |
|
||||
237 237 | # B006 and B008
|
||||
238 238 | # We should handle arbitrary nesting of these B008.
|
||||
239 |-def nested_combo(a=[float(3), dt.datetime.now()]):
|
||||
239 |+def nested_combo(a=None):
|
||||
240 |+ if a is None:
|
||||
241 |+ a = [float(3), dt.datetime.now()]
|
||||
240 242 | pass
|
||||
241 243 |
|
||||
242 244 |
|
||||
|
||||
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
271 | def mutable_annotations(
|
||||
272 | a: list[int] | None = [],
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
| ^^ B006
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
269 269 |
|
||||
270 270 |
|
||||
271 271 | def mutable_annotations(
|
||||
272 |- a: list[int] | None = [],
|
||||
272 |+ a: list[int] | None = None,
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if a is None:
|
||||
278 |+ a = []
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
271 | def mutable_annotations(
|
||||
272 | a: list[int] | None = [],
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
| ^^ B006
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
270 270 |
|
||||
271 271 | def mutable_annotations(
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 |- b: Optional[Dict[int, int]] = {},
|
||||
273 |+ b: Optional[Dict[int, int]] = None,
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if b is None:
|
||||
278 |+ b = {}
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
272 | a: list[int] | None = [],
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 | ):
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
271 271 | def mutable_annotations(
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
276 276 | ):
|
||||
277 |+ if c is None:
|
||||
278 |+ c = set()
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
273 | b: Optional[Dict[int, int]] = {},
|
||||
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
276 | ):
|
||||
277 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
272 272 | a: list[int] | None = [],
|
||||
273 273 | b: Optional[Dict[int, int]] = {},
|
||||
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
276 276 | ):
|
||||
277 |+ if d is None:
|
||||
278 |+ d = set()
|
||||
277 279 | pass
|
||||
278 280 |
|
||||
279 281 |
|
||||
|
||||
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
280 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
281 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
277 277 | pass
|
||||
278 278 |
|
||||
279 279 |
|
||||
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
280 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
281 281 | """Docstring"""
|
||||
282 |+ if value is None:
|
||||
283 |+ value = {}
|
||||
273 273 |
|
||||
274 274 |
|
||||
275 275 | def mutable_annotations(
|
||||
276 |- a: list[int] | None = [],
|
||||
276 |+ a: list[int] | None = None,
|
||||
277 277 | b: Optional[Dict[int, int]] = {},
|
||||
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 280 | ):
|
||||
281 |+ if a is None:
|
||||
282 |+ a = []
|
||||
281 283 | pass
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
275 | def mutable_annotations(
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
| ^^ B006
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
274 274 |
|
||||
275 275 | def mutable_annotations(
|
||||
276 276 | a: list[int] | None = [],
|
||||
277 |- b: Optional[Dict[int, int]] = {},
|
||||
277 |+ b: Optional[Dict[int, int]] = None,
|
||||
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 280 | ):
|
||||
281 |+ if b is None:
|
||||
282 |+ b = {}
|
||||
281 283 | pass
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
276 | a: list[int] | None = [],
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 | ):
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
275 275 | def mutable_annotations(
|
||||
276 276 | a: list[int] | None = [],
|
||||
277 277 | b: Optional[Dict[int, int]] = {},
|
||||
278 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
278 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
280 280 | ):
|
||||
281 |+ if c is None:
|
||||
282 |+ c = set()
|
||||
281 283 | pass
|
||||
282 284 |
|
||||
283 285 |
|
||||
|
||||
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
277 | b: Optional[Dict[int, int]] = {},
|
||||
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
| ^^^^^ B006
|
||||
280 | ):
|
||||
281 | pass
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
276 276 | a: list[int] | None = [],
|
||||
277 277 | b: Optional[Dict[int, int]] = {},
|
||||
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
||||
279 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
|
||||
280 280 | ):
|
||||
281 |+ if d is None:
|
||||
282 |+ d = set()
|
||||
281 283 | pass
|
||||
282 284 |
|
||||
283 285 |
|
||||
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
284 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
285 | """Docstring"""
|
||||
286 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
281 281 | """Docstring"""
|
||||
281 281 | pass
|
||||
282 282 |
|
||||
283 283 |
|
||||
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
@@ -420,59 +398,81 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
|
||||
285 285 | """Docstring"""
|
||||
286 |+ if value is None:
|
||||
287 |+ value = {}
|
||||
286 288 | ...
|
||||
286 288 |
|
||||
287 289 |
|
||||
288 290 |
|
||||
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
|
||||
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
289 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
288 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
290 | """Docstring"""; ...
|
||||
289 | """Docstring"""
|
||||
290 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
285 285 | """Docstring"""
|
||||
286 286 |
|
||||
287 287 |
|
||||
288 |-def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
288 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
289 289 | """Docstring"""
|
||||
290 |+ if value is None:
|
||||
291 |+ value = {}
|
||||
290 292 | ...
|
||||
291 293 |
|
||||
292 294 |
|
||||
|
||||
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
293 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
294 | """Docstring"""; \
|
||||
295 | ...
|
||||
294 | """Docstring"""; ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
298 | def single_line_func_wrong(value: dict[str, str] = {
|
||||
297 | def single_line_func_wrong(value: dict[str, str] = {}):
|
||||
| ^^ B006
|
||||
298 | """Docstring"""; \
|
||||
299 | ...
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
||||
|
|
||||
302 | def single_line_func_wrong(value: dict[str, str] = {
|
||||
| ____________________________________________________^
|
||||
299 | | # This is a comment
|
||||
300 | | }):
|
||||
303 | | # This is a comment
|
||||
304 | | }):
|
||||
| |_^ B006
|
||||
301 | """Docstring"""
|
||||
305 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
ℹ Possible fix
|
||||
295 295 | ...
|
||||
296 296 |
|
||||
297 297 |
|
||||
298 |-def single_line_func_wrong(value: dict[str, str] = {
|
||||
299 |- # This is a comment
|
||||
300 |-}):
|
||||
298 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
301 299 | """Docstring"""
|
||||
300 |+ if value is None:
|
||||
301 |+ value = {}
|
||||
302 302 |
|
||||
303 303 |
|
||||
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
299 299 | ...
|
||||
300 300 |
|
||||
301 301 |
|
||||
302 |-def single_line_func_wrong(value: dict[str, str] = {
|
||||
303 |- # This is a comment
|
||||
304 |-}):
|
||||
302 |+def single_line_func_wrong(value: dict[str, str] = None):
|
||||
305 303 | """Docstring"""
|
||||
304 |+ if value is None:
|
||||
305 |+ value = {}
|
||||
306 306 |
|
||||
307 307 |
|
||||
308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
|
||||
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
||||
B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults
|
||||
|
|
||||
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
|
||||
| ^^ B006
|
||||
305 | : \
|
||||
306 | """Docstring"""
|
||||
309 | : \
|
||||
310 | """Docstring"""
|
||||
|
|
||||
= help: Replace with `None`; initialize within function
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user