Compare commits

..

1 Commits

Author SHA1 Message Date
Charlie Marsh
5120b36b67 Consider alternate split points for line-too-long exemptions 2023-11-30 22:53:50 -05:00
116 changed files with 3204 additions and 5811 deletions

View File

@@ -1,99 +1,5 @@
# Changelog
## 0.1.7
### Preview features
- Implement multiline dictionary and list hugging for preview style ([#8293](https://github.com/astral-sh/ruff/pull/8293))
- Implement the `fix_power_op_line_length` preview style ([#8947](https://github.com/astral-sh/ruff/pull/8947))
- Use Python version to determine typing rewrite safety ([#8919](https://github.com/astral-sh/ruff/pull/8919))
- \[`flake8-annotations`\] Enable auto-return-type involving `Optional` and `Union` annotations ([#8885](https://github.com/astral-sh/ruff/pull/8885))
- \[`flake8-bandit`\] Implement `django-raw-sql` (`S611`) ([#8651](https://github.com/astral-sh/ruff/pull/8651))
- \[`flake8-bandit`\] Implement `tarfile-unsafe-members` (`S202`) ([#8829](https://github.com/astral-sh/ruff/pull/8829))
- \[`flake8-pyi`\] Implement fix for `unnecessary-literal-union` (`PYI030`) ([#7934](https://github.com/astral-sh/ruff/pull/7934))
- \[`flake8-simplify`\] Extend `dict-get-with-none-default` (`SIM910`) to non-literals ([#8762](https://github.com/astral-sh/ruff/pull/8762))
- \[`pylint`\] - add `unnecessary-list-index-lookup` (`PLR1736`) + autofix ([#7999](https://github.com/astral-sh/ruff/pull/7999))
- \[`pylint`\] - implement R0202 and R0203 with autofixes ([#8335](https://github.com/astral-sh/ruff/pull/8335))
- \[`pylint`\] Implement `repeated-keyword` (`PLe1132`) ([#8706](https://github.com/astral-sh/ruff/pull/8706))
- \[`pylint`\] Implement `too-many-positional` (`PLR0917`) ([#8995](https://github.com/astral-sh/ruff/pull/8995))
- \[`pylint`\] Implement `unnecessary-dict-index-lookup` (`PLR1733`) ([#8036](https://github.com/astral-sh/ruff/pull/8036))
- \[`refurb`\] Implement `redundant-log-base` (`FURB163`) ([#8842](https://github.com/astral-sh/ruff/pull/8842))
### Rule changes
- \[`flake8-boolean-trap`\] Allow booleans in `@override` methods ([#8882](https://github.com/astral-sh/ruff/pull/8882))
- \[`flake8-bugbear`\] Avoid `B015`,`B018` for last expression in a cell ([#8815](https://github.com/astral-sh/ruff/pull/8815))
- \[`flake8-pie`\] Allow ellipses for enum values in stub files ([#8825](https://github.com/astral-sh/ruff/pull/8825))
- \[`flake8-pyi`\] Check PEP 695 type aliases for `snake-case-type-alias` and `t-suffixed-type-alias` ([#8966](https://github.com/astral-sh/ruff/pull/8966))
- \[`flake8-pyi`\] Check for kwarg and vararg `NoReturn` type annotations ([#8948](https://github.com/astral-sh/ruff/pull/8948))
- \[`flake8-simplify`\] Omit select context managers from `SIM117` ([#8801](https://github.com/astral-sh/ruff/pull/8801))
- \[`pep8-naming`\] Allow Django model loads in `non-lowercase-variable-in-function` (`N806`) ([#8917](https://github.com/astral-sh/ruff/pull/8917))
- \[`pycodestyle`\] Avoid `E703` for last expression in a cell ([#8821](https://github.com/astral-sh/ruff/pull/8821))
- \[`pycodestyle`\] Update `E402` to work at cell level for notebooks ([#8872](https://github.com/astral-sh/ruff/pull/8872))
- \[`pydocstyle`\] Avoid `D100` for Jupyter Notebooks ([#8816](https://github.com/astral-sh/ruff/pull/8816))
- \[`pylint`\] Implement fix for `unspecified-encoding` (`PLW1514`) ([#8928](https://github.com/astral-sh/ruff/pull/8928))
### Formatter
- Avoid unstable formatting in ellipsis-only body with trailing comment ([#8984](https://github.com/astral-sh/ruff/pull/8984))
- Inline trailing comments for type alias similar to assignments ([#8941](https://github.com/astral-sh/ruff/pull/8941))
- Insert trailing comma when function breaks with single argument ([#8921](https://github.com/astral-sh/ruff/pull/8921))
### CLI
- Update `ruff check` and `ruff format` to default to the current directory ([#8791](https://github.com/astral-sh/ruff/pull/8791))
- Stop at the first resolved parent configuration ([#8864](https://github.com/astral-sh/ruff/pull/8864))
### Configuration
- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998))
- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812))
- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
### Bug fixes
- Add support for `@functools.singledispatch` ([#8934](https://github.com/astral-sh/ruff/pull/8934))
- Avoid off-by-one error in stripping noqa following multi-byte char ([#8979](https://github.com/astral-sh/ruff/pull/8979))
- Avoid off-by-one error in with-item named expressions ([#8915](https://github.com/astral-sh/ruff/pull/8915))
- Avoid syntax error via invalid ur string prefix ([#8971](https://github.com/astral-sh/ruff/pull/8971))
- Avoid underflow in `get_model` matching ([#8965](https://github.com/astral-sh/ruff/pull/8965))
- Avoid unnecessary index diagnostics when value is modified ([#8970](https://github.com/astral-sh/ruff/pull/8970))
- Convert over-indentation rule to use number of characters ([#8983](https://github.com/astral-sh/ruff/pull/8983))
- Detect implicit returns in auto-return-types ([#8952](https://github.com/astral-sh/ruff/pull/8952))
- Fix start >= end error in over-indentation ([#8982](https://github.com/astral-sh/ruff/pull/8982))
- Ignore `@overload` and `@override` methods for too-many-arguments checks ([#8954](https://github.com/astral-sh/ruff/pull/8954))
- Lexer start of line is false only for `Mode::Expression` ([#8880](https://github.com/astral-sh/ruff/pull/8880))
- Mark `pydantic_settings.BaseSettings` as having default copy semantics ([#8793](https://github.com/astral-sh/ruff/pull/8793))
- Respect dictionary unpacking in `NamedTuple` assignments ([#8810](https://github.com/astral-sh/ruff/pull/8810))
- Respect local subclasses in `flake8-type-checking` ([#8768](https://github.com/astral-sh/ruff/pull/8768))
- Support type alias statements in simple statement positions ([#8916](https://github.com/astral-sh/ruff/pull/8916))
- \[`flake8-annotations`\] Avoid filtering out un-representable types in return annotation ([#8881](https://github.com/astral-sh/ruff/pull/8881))
- \[`flake8-pie`\] Retain extra ellipses in protocols and abstract methods ([#8769](https://github.com/astral-sh/ruff/pull/8769))
- \[`flake8-pyi`\] Respect local enum subclasses in `simple-defaults` (`PYI052`) ([#8767](https://github.com/astral-sh/ruff/pull/8767))
- \[`flake8-trio`\] Use correct range for `TRIO115` fix ([#8933](https://github.com/astral-sh/ruff/pull/8933))
- \[`flake8-trio`\] Use full arguments range for zero-sleep-call ([#8936](https://github.com/astral-sh/ruff/pull/8936))
- \[`isort`\] fix: mark `__main__` as first-party import ([#8805](https://github.com/astral-sh/ruff/pull/8805))
- \[`pep8-naming`\] Avoid `N806` errors for type alias statements ([#8785](https://github.com/astral-sh/ruff/pull/8785))
- \[`perflint`\] Avoid `PERF101` if there's an append in loop body ([#8809](https://github.com/astral-sh/ruff/pull/8809))
- \[`pycodestyle`\] Allow space-before-colon after end-of-slice ([#8838](https://github.com/astral-sh/ruff/pull/8838))
- \[`pydocstyle`\] Avoid non-character breaks in `over-indentation` (`D208`) ([#8866](https://github.com/astral-sh/ruff/pull/8866))
- \[`pydocstyle`\] Ignore underlines when determining docstring logical lines ([#8929](https://github.com/astral-sh/ruff/pull/8929))
- \[`pylint`\] Extend `self-assigning-variable` to multi-target assignments ([#8839](https://github.com/astral-sh/ruff/pull/8839))
- \[`tryceratops`\] Avoid repeated triggers in nested `tryceratops` diagnostics ([#8772](https://github.com/astral-sh/ruff/pull/8772))
### Documentation
- Add advice for fixing RUF008 when mutability is not desired ([#8853](https://github.com/astral-sh/ruff/pull/8853))
- Added the command to run ruff using pkgx to the installation.md ([#8955](https://github.com/astral-sh/ruff/pull/8955))
- Document fix safety for flake8-comprehensions and some pyupgrade rules ([#8918](https://github.com/astral-sh/ruff/pull/8918))
- Fix doc formatting for zero-sleep-call ([#8937](https://github.com/astral-sh/ruff/pull/8937))
- Remove duplicate imports from os-stat documentation ([#8930](https://github.com/astral-sh/ruff/pull/8930))
- Replace generated reference to MkDocs ([#8806](https://github.com/astral-sh/ruff/pull/8806))
- Update Arch Linux package URL in installation.md ([#8802](https://github.com/astral-sh/ruff/pull/8802))
- \[`flake8-pyi`\] Fix error in `t-suffixed-type-alias` (`PYI043`) example ([#8963](https://github.com/astral-sh/ruff/pull/8963))
- \[`flake8-pyi`\] Improve motivation for `custom-type-var-return-type` (`PYI019`) ([#8766](https://github.com/astral-sh/ruff/pull/8766))
## 0.1.6
### Preview features

64
Cargo.lock generated
View File

@@ -808,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.7"
version = "0.1.6"
dependencies = [
"anyhow",
"clap",
@@ -848,18 +848,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
version = "2.11.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
checksum = "fb5fd9bcbe8b1087cbd395b51498c01bc997cef73e778a80b77a811af5e2d29f"
dependencies = [
"autocfg",
]
@@ -987,9 +987,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -1170,9 +1170,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.66"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
dependencies = [
"wasm-bindgen",
]
@@ -1622,9 +1622,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "petgraph"
@@ -2062,7 +2062,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.7"
version = "0.1.6"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2198,7 +2198,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.7"
version = "0.1.6"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2450,7 +2450,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.7"
version = "0.1.6"
dependencies = [
"anyhow",
"clap",
@@ -2618,9 +2618,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.16"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -2630,9 +2630,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.16"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
dependencies = [
"proc-macro2",
"quote",
@@ -3334,9 +3334,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.9.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3"
dependencies = [
"base64",
"flate2",
@@ -3350,9 +3350,9 @@ dependencies = [
[[package]]
name = "url"
version = "2.5.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
"idna",
@@ -3461,9 +3461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.89"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -3471,9 +3471,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
dependencies = [
"bumpalo",
"log",
@@ -3498,9 +3498,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3508,9 +3508,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [
"proc-macro2",
"quote",
@@ -3521,9 +3521,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
[[package]]
name = "wasm-bindgen-test"

View File

@@ -33,7 +33,7 @@ proc-macro2 = { version = "1.0.70" }
quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.190", features = ["derive"] }
serde_json = { version = "1.0.108" }
shellexpand = { version = "3.0.0" }

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.7
rev: v0.1.6
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -34,8 +34,8 @@ harness = false
once_cell.workspace = true
serde.workspace = true
serde_json.workspace = true
url = "2.5.0"
ureq = "2.9.1"
url = "2.3.1"
ureq = "2.8.0"
criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { version="2.3.3", default-features = false, optional = true}

View File

@@ -4,7 +4,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions};
use ruff_python_formatter::{format_module_ast, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
@@ -69,8 +69,7 @@ fn benchmark_formatter(criterion: &mut Criterion) {
.expect("Input to be a valid python program");
b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name()))
.with_preview(PreviewMode::Enabled);
let options = PyFormatOptions::from_extension(Path::new(case.name()));
let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.7"
version = "0.1.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -69,7 +69,7 @@ insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { version = "0.4.0" }
tempfile = "3.8.1"
test-case = { workspace = true }
ureq = { version = "2.9.1", features = [] }
ureq = { version = "2.8.0", features = [] }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = "0.1.39"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.7"
version = "0.1.6"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -39,18 +39,3 @@ def func():
for i in range(1110):
if True:
break
# TODO(charlie): The `pass` here does not get properly redirected to the top of the
# loop, unlike below.
def func():
for i in range(5):
pass
else:
return 1
def func():
for i in range(5):
pass
else:
return 1
x = 1

View File

@@ -129,11 +129,3 @@ def func():
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")
def func(point):
match point:
case (0, 0):
print("Origin")
case foo:
raise ValueError("oops")

View File

@@ -63,87 +63,3 @@ def func(x: int):
return "str"
else:
return None
def func(x: int):
if x:
return 1
def func():
x = 1
def func(x: int):
if x > 0:
return 1
def func(x: int):
match x:
case [1, 2, 3]:
return 1
case 4 as y:
return "foo"
def func(x: int):
for i in range(5):
if i > 0:
return 1
def func(x: int):
for i in range(5):
if i > 0:
return 1
else:
return 4
def func(x: int):
for i in range(5):
if i > 0:
break
else:
return 4
def func(x: int):
try:
pass
except:
return 1
def func(x: int):
try:
pass
except:
return 1
finally:
return 2
def func(x: int):
try:
pass
except:
return 1
else:
return 2
def func(x: int):
try:
return 1
except:
return 2
else:
pass
def func(x: int):
while x > 0:
break
return 1

View File

@@ -22,7 +22,3 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -22,7 +22,3 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -21,7 +21,3 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -21,7 +21,3 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -10,14 +10,3 @@ def foo_no_return_typing_extensions(
def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050
def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
def foo_never(arg: Never): ...
def foo_args(*args: NoReturn): ... # Error: PYI050
def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_args(*args: int): ...
def foo_int_kwargs(**kwargs: int): ...
def foo_int_args_kwargs(*args: int, **kwargs: int): ...
def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
def foo_args_never(*args: Never): ...
def foo_kwargs_never(**kwargs: Never): ...
def foo_args_kwargs_never(*args: Never, **kwargs: Never): ...

View File

@@ -52,6 +52,3 @@ def model_assign() -> None:
Bad = import_string("django.core.exceptions.ValidationError") # N806
ValidationError = import_string("django.core.exceptions.ValidationError") # OK
Bad = apps.get_model() # N806
Bad = apps.get_model(model_name="Stream") # N806

View File

@@ -43,6 +43,3 @@ regex = '''
''' # noqa
regex = '\\\_'
#: W605:1:7
u'foo\ bar'

View File

@@ -1,54 +1,40 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = f'\.png$'
regex = '\.png$'
#: W605:2:1
regex = f'''
regex = '''
\.png$
'''
#: W605:2:6
f(
f'\_'
'\_'
)
#: W605:4:6
f"""
"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
regex = r'\.png$'
regex = '\\.png$'
regex = r'''
\.png$
'''
regex = fr'''
regex = r'''
\\.png$
'''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
s = '\\'
regex = '\w' # noqa
regex = '''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -0,0 +1,54 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = f'\.png$'
#: W605:2:1
regex = f'''
\.png$
'''
#: W605:2:6
f(
f'\_'
)
#: W605:4:6
f"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
#: Okay
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
\.png$
'''
regex = fr'''
\\.png$
'''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -1,16 +1,5 @@
"""
Author
"""
class Platform:
""" Remove sampler
Args:
    Returns:
"""
def memory_test():
"""
   参数含义precision:精确到小数点后几位
"""

View File

@@ -32,16 +32,3 @@ def f(x, y, z, *, u, v, w): # Too many arguments (6/5)
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (9/5)
pass
from typing import override, overload
@override
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass
@overload
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass

View File

@@ -1,30 +0,0 @@
def f(x, y, z, t, u, v, w, r): # Too many positional arguments (8/3)
pass
def f(x): # OK
pass
def f(x, y, z, _t, _u, _v, _w, r): # OK (underscore-prefixed names are ignored
pass
def f(x, y, z, *, u=1, v=1, r=1): # OK
pass
def f(x=1, y=1, z=1): # OK
pass
def f(x, y, z, /, u, v, w): # Too many positional arguments (6/3)
pass
def f(x, y, z, *, u, v, w): # OK
pass
def f(x, y, z, a, b, c, *, u, v, w): # Too many positional arguments (6/3)
pass

View File

@@ -1,10 +0,0 @@
# Too many positional arguments (7/4) for max_positional=4
# OK for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, skip_t, skip_u, skip_v):
pass
# Too many positional arguments (7/4) for max_args=4
# Too many positional arguments (7/3) for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, t, u, v):
pass

View File

@@ -1,40 +0,0 @@
FRUITS = {"apple": 1, "orange": 10, "berry": 22}
def fix_these():
[FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
{FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
{fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
for fruit_name, fruit_count in FRUITS.items():
print(FRUITS[fruit_name]) # PLR1733
blah = FRUITS[fruit_name] # PLR1733
assert FRUITS[fruit_name] == "pear" # PLR1733
def dont_fix_these():
# once there is an assignment to the dict[index], we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
FRUITS[fruit_name] = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the key, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
fruit_name = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the value, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
if fruit_count < 5:
fruit_count = -fruit_count
assert FRUITS[fruit_name] == 0 # OK
def value_intentionally_unused():
[FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()] # OK
{FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
{fruit_name: FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
for fruit_name, _ in FRUITS.items():
print(FRUITS[fruit_name]) # OK
blah = FRUITS[fruit_name] # OK
assert FRUITS[fruit_name] == "pear" # OK

View File

@@ -12,7 +12,7 @@ def fix_these():
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
for index, letter in builtins.enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
@@ -22,43 +22,38 @@ def fix_these():
def dont_fix_these():
# once there is an assignment to the sequence[index], we stop emitting diagnostics
for index, letter in enumerate(letters):
letters[index] = "d" # OK
letters[index] += "e" # OK
assert letters[index] == "de" # OK
letters[index] = "d" # Ok
letters[index] += "e" # Ok
assert letters[index] == "de" # Ok
# once there is an assignment to the index, we stop emitting diagnostics
for index, letter in enumerate(letters):
index += 1 # OK
print(letters[index]) # OK
index += 1 # Ok
print(letters[index]) # Ok
# once there is an assignment to the sequence, we stop emitting diagnostics
for index, letter in enumerate(letters):
letters = ["d", "e", "f"] # OK
print(letters[index]) # OK
# once there is an assignment to the value, we stop emitting diagnostics
for index, letter in enumerate(letters):
letter = "d"
print(letters[index]) # OK
letters = ["d", "e", "f"] # Ok
print(letters[index]) # Ok
# once there is an deletion from or of the sequence or index, we stop emitting diagnostics
for index, letter in enumerate(letters):
del letters[index] # OK
print(letters[index]) # OK
del letters[index] # Ok
print(letters[index]) # Ok
for index, letter in enumerate(letters):
del letters # OK
print(letters[index]) # OK
del letters # Ok
print(letters[index]) # Ok
for index, letter in enumerate(letters):
del index # OK
print(letters[index]) # OK
del index # Ok
print(letters[index]) # Ok
def value_intentionally_unused():
[letters[index] for index, _ in enumerate(letters)] # OK
{letters[index] for index, _ in enumerate(letters)} # OK
{index: letters[index] for index, _ in enumerate(letters)} # OK
[letters[index] for index, _ in enumerate(letters)] # Ok
{letters[index] for index, _ in enumerate(letters)} # Ok
{index: letters[index] for index, _ in enumerate(letters)} # Ok
for index, _ in enumerate(letters):
print(letters[index]) # OK
blah = letters[index] # OK
letters[index] = "d" # OK
print(letters[index]) # Ok
blah = letters[index] # Ok
letters[index] = "d" # Ok

View File

@@ -42,30 +42,3 @@ tempfile.SpooledTemporaryFile(0, "w", encoding="utf-8")
tempfile.SpooledTemporaryFile(0, "w", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "wb")
tempfile.SpooledTemporaryFile(0, )
open("test.txt",)
open()
open(
"test.txt", # comment
)
open(
"test.txt",
# comment
)
open(("test.txt"),)
open(
("test.txt"), # comment
)
open(
("test.txt"),
# comment
)
open((("test.txt")),)
open(
(("test.txt")), # comment
)
open(
(("test.txt")),
# comment
)

View File

@@ -23,6 +23,3 @@ print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 comment
print(a) # noqa: E501, F821 comment
print(a) # comment with unicode µ # noqa: E501
print(a) # comment with unicode µ # noqa: E501, F821

View File

@@ -1333,9 +1333,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1363,9 +1360,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1392,9 +1386,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
@@ -1422,9 +1413,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}

View File

@@ -248,10 +248,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}
if checker.enabled(Rule::TooManyArguments) {
pylint::rules::too_many_arguments(checker, function_def);
}
if checker.enabled(Rule::TooManyPositional) {
pylint::rules::too_many_positional(checker, parameters, stmt);
pylint::rules::too_many_arguments(checker, parameters, stmt);
}
if checker.enabled(Rule::TooManyReturnStatements) {
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
@@ -1283,9 +1280,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup(checker, for_stmt);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
@@ -1537,14 +1531,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
if checker.enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, name);
}
if checker.enabled(Rule::TSuffixedTypeAlias) {
flake8_pyi::rules::t_suffixed_type_alias(checker, name);
}
}
Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => {
if checker.enabled(Rule::GlobalStatement) {
for target in targets {

View File

@@ -816,7 +816,7 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
// Step 0: Pre-processing
if !self.semantic.in_f_string()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_literal()
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
@@ -1198,7 +1198,7 @@ where
) {
// Ex) Literal["Class"]
Some(typing::SubscriptKind::Literal) => {
self.semantic.flags |= SemanticModelFlags::TYPING_LITERAL;
self.semantic.flags |= SemanticModelFlags::LITERAL;
self.visit_expr(slice);
self.visit_expr_context(ctx);
@@ -1239,7 +1239,7 @@ where
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if self.semantic.in_type_definition()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_literal()
&& !self.semantic.in_f_string()
{
self.deferred.string_type_definitions.push((

View File

@@ -3,10 +3,10 @@
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
use crate::noqa;
@@ -200,11 +200,17 @@ fn delete_noqa(range: TextRange, locator: &Locator) -> Edit {
// Compute the leading space.
let prefix = locator.slice(TextRange::new(line_range.start(), range.start()));
let leading_space_len = prefix.text_len() - prefix.trim_whitespace_end().text_len();
let leading_space = prefix
.rfind(|c: char| !c.is_whitespace())
.map_or(prefix.len(), |i| prefix.len() - i - 1);
let leading_space_len = TextSize::try_from(leading_space).unwrap();
// Compute the trailing space.
let suffix = locator.slice(TextRange::new(range.end(), line_range.end()));
let trailing_space_len = suffix.text_len() - suffix.trim_whitespace_start().text_len();
let trailing_space = suffix
.find(|c: char| !c.is_whitespace())
.map_or(suffix.len(), |i| i);
let trailing_space_len = TextSize::try_from(trailing_space).unwrap();
// Ex) `# noqa`
if line_range

View File

@@ -254,14 +254,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),

View File

@@ -3,14 +3,12 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
SimpleTokenizer,
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -140,32 +138,6 @@ pub(crate) fn remove_argument<T: Ranged>(
}
}
/// Generic function to add arguments or keyword arguments to function calls.
pub(crate) fn add_argument(
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range(
match last {
ArgOrKeyword::Arg(arg) => arg.into(),
ArgOrKeyword::Keyword(keyword) => (&keyword.value).into(),
},
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end())
} else {
// Case 2: no arguments. Add argument, without any trailing comma.
Edit::insertion(argument.to_string(), arguments.start() + TextSize::from(1))
}
}
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value

View File

@@ -1,9 +1,10 @@
use itertools::Itertools;
use ruff_diagnostics::Edit;
use rustc_hash::FxHashSet;
use ruff_diagnostics::Edit;
use crate::importer::{ImportRequest, Importer};
use ruff_python_ast::helpers::{
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
@@ -12,7 +13,6 @@ use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_text_size::{TextRange, TextSize};
use crate::importer::{ImportRequest, Importer};
use crate::settings::types::PythonVersion;
/// Return the name of the function, if it's overloaded.
@@ -48,19 +48,14 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function.body);
// Ignore generators.
if visitor.is_generator {
return None;
}
visitor.returns
};
// Determine the return type of the first `return` statement.
let Some((return_statement, returns)) = returns.split_first() else {
return Some(AutoPythonType::Atom(PythonType::None));
};
let (return_statement, returns) = returns.split_first()?;
let mut return_type = return_statement.value.as_deref().map_or(
ResolvedPythonType::Atom(PythonType::None),
ResolvedPythonType::from,
@@ -74,16 +69,6 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
));
}
// If the function has an implicit return, union with `None`, as in:
// ```python
// def func(x: int):
// if x > 0:
// return 1
// ```
if implicit_return(function) {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}
match return_type {
ResolvedPythonType::Atom(python_type) => Some(AutoPythonType::Atom(python_type)),
ResolvedPythonType::Union(python_types) => Some(AutoPythonType::Union(python_types)),

View File

@@ -200,231 +200,4 @@ auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public f
61 61 | return 1
62 62 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
65 65 | return None
66 66 |
67 67 |
68 |-def func(x: int):
68 |+def func(x: int) -> int | None:
69 69 | if x:
70 70 | return 1
71 71 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
74 74 | x = 1
75 75 |
76 76 |
77 |-def func(x: int):
77 |+def func(x: int) -> int | None:
78 78 | if x > 0:
79 79 | return 1
80 80 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `str | int`
Unsafe fix
79 79 | return 1
80 80 |
81 81 |
82 |-def func(x: int):
82 |+def func(x: int) -> str | int:
83 83 | match x:
84 84 | case [1, 2, 3]:
85 85 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
87 87 | return "foo"
88 88 |
89 89 |
90 |-def func(x: int):
90 |+def func(x: int) -> int | None:
91 91 | for i in range(5):
92 92 | if i > 0:
93 93 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
101 101 | return 4
102 102 |
103 103 |
104 |-def func(x: int):
104 |+def func(x: int) -> int | None:
105 105 | for i in range(5):
106 106 | if i > 0:
107 107 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `int | None`
Unsafe fix
109 109 | return 4
110 110 |
111 111 |
112 |-def func(x: int):
112 |+def func(x: int) -> int | None:
113 113 | try:
114 114 | pass
115 115 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `int | None`
Unsafe fix
143 143 | pass
144 144 |
145 145 |
146 |-def func(x: int):
146 |+def func(x: int) -> int | None:
147 147 | while x > 0:
148 148 | break
149 149 | return 1

View File

@@ -220,266 +220,4 @@ auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public f
61 62 | return 1
62 63 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
65 66 | return None
66 67 |
67 68 |
68 |-def func(x: int):
69 |+def func(x: int) -> Optional[int]:
69 70 | if x:
70 71 | return 1
71 72 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
74 75 | x = 1
75 76 |
76 77 |
77 |-def func(x: int):
78 |+def func(x: int) -> Optional[int]:
78 79 | if x > 0:
79 80 | return 1
80 81 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `Union[str | int]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
79 80 | return 1
80 81 |
81 82 |
82 |-def func(x: int):
83 |+def func(x: int) -> Union[str | int]:
83 84 | match x:
84 85 | case [1, 2, 3]:
85 86 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
87 88 | return "foo"
88 89 |
89 90 |
90 |-def func(x: int):
91 |+def func(x: int) -> Optional[int]:
91 92 | for i in range(5):
92 93 | if i > 0:
93 94 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
101 102 | return 4
102 103 |
103 104 |
104 |-def func(x: int):
105 |+def func(x: int) -> Optional[int]:
105 106 | for i in range(5):
106 107 | if i > 0:
107 108 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
109 110 | return 4
110 111 |
111 112 |
112 |-def func(x: int):
113 |+def func(x: int) -> Optional[int]:
113 114 | try:
114 115 | pass
115 116 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
143 144 | pass
144 145 |
145 146 |
146 |-def func(x: int):
147 |+def func(x: int) -> Optional[int]:
147 148 | while x > 0:
148 149 | break
149 150 | return 1

View File

@@ -1,24 +1,14 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
annotation_presence.py:5:5: ANN201 [*] Missing return type annotation for public function `foo`
annotation_presence.py:5:5: ANN201 Missing return type annotation for public function `foo`
|
4 | # Error
5 | def foo(a, b):
| ^^^ ANN201
6 | pass
|
= help: Add return type annotation: `None`
Unsafe fix
2 2 | from typing_extensions import override
3 3 |
4 4 | # Error
5 |-def foo(a, b):
5 |+def foo(a, b) -> None:
6 6 | pass
7 7 |
8 8 |
= help: Add return type annotation
annotation_presence.py:5:9: ANN001 Missing type annotation for function argument `a`
|
@@ -36,24 +26,14 @@ annotation_presence.py:5:12: ANN001 Missing type annotation for function argumen
6 | pass
|
annotation_presence.py:10:5: ANN201 [*] Missing return type annotation for public function `foo`
annotation_presence.py:10:5: ANN201 Missing return type annotation for public function `foo`
|
9 | # Error
10 | def foo(a: int, b):
| ^^^ ANN201
11 | pass
|
= help: Add return type annotation: `None`
Unsafe fix
7 7 |
8 8 |
9 9 | # Error
10 |-def foo(a: int, b):
10 |+def foo(a: int, b) -> None:
11 11 | pass
12 12 |
13 13 |
= help: Add return type annotation
annotation_presence.py:10:17: ANN001 Missing type annotation for function argument `b`
|
@@ -71,43 +51,23 @@ annotation_presence.py:15:17: ANN001 Missing type annotation for function argume
16 | pass
|
annotation_presence.py:20:5: ANN201 [*] Missing return type annotation for public function `foo`
annotation_presence.py:20:5: ANN201 Missing return type annotation for public function `foo`
|
19 | # Error
20 | def foo(a: int, b: int):
| ^^^ ANN201
21 | pass
|
= help: Add return type annotation: `None`
= help: Add return type annotation
Unsafe fix
17 17 |
18 18 |
19 19 | # Error
20 |-def foo(a: int, b: int):
20 |+def foo(a: int, b: int) -> None:
21 21 | pass
22 22 |
23 23 |
annotation_presence.py:25:5: ANN201 [*] Missing return type annotation for public function `foo`
annotation_presence.py:25:5: ANN201 Missing return type annotation for public function `foo`
|
24 | # Error
25 | def foo():
| ^^^ ANN201
26 | pass
|
= help: Add return type annotation: `None`
Unsafe fix
22 22 |
23 23 |
24 24 | # Error
25 |-def foo():
25 |+def foo() -> None:
26 26 | pass
27 27 |
28 28 |
= help: Add return type annotation
annotation_presence.py:45:12: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|

View File

@@ -1,23 +1,13 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
ignore_fully_untyped.py:24:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_1`
ignore_fully_untyped.py:24:5: ANN201 Missing return type annotation for public function `error_partially_typed_1`
|
24 | def error_partially_typed_1(a: int, b):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
25 | pass
|
= help: Add return type annotation: `None`
Unsafe fix
21 21 | pass
22 22 |
23 23 |
24 |-def error_partially_typed_1(a: int, b):
24 |+def error_partially_typed_1(a: int, b) -> None:
25 25 | pass
26 26 |
27 27 |
= help: Add return type annotation
ignore_fully_untyped.py:24:37: ANN001 Missing type annotation for function argument `b`
|
@@ -33,25 +23,15 @@ ignore_fully_untyped.py:28:37: ANN001 Missing type annotation for function argum
29 | pass
|
ignore_fully_untyped.py:32:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_3`
ignore_fully_untyped.py:32:5: ANN201 Missing return type annotation for public function `error_partially_typed_3`
|
32 | def error_partially_typed_3(a: int, b: int):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
33 | pass
|
= help: Add return type annotation: `None`
= help: Add return type annotation
Unsafe fix
29 29 | pass
30 30 |
31 31 |
32 |-def error_partially_typed_3(a: int, b: int):
32 |+def error_partially_typed_3(a: int, b: int) -> None:
33 33 | pass
34 34 |
35 35 |
ignore_fully_untyped.py:43:9: ANN201 [*] Missing return type annotation for public function `error_typed_self`
ignore_fully_untyped.py:43:9: ANN201 Missing return type annotation for public function `error_typed_self`
|
41 | pass
42 |
@@ -59,14 +39,6 @@ ignore_fully_untyped.py:43:9: ANN201 [*] Missing return type annotation for publ
| ^^^^^^^^^^^^^^^^ ANN201
44 | pass
|
= help: Add return type annotation: `None`
Unsafe fix
40 40 | def ok_untyped_method(self):
41 41 | pass
42 42 |
43 |- def error_typed_self(self: X):
43 |+ def error_typed_self(self: X) -> None:
44 44 | pass
= help: Add return type annotation

View File

@@ -41,24 +41,14 @@ mypy_init_return.py:11:9: ANN204 [*] Missing return type annotation for special
13 13 |
14 14 |
mypy_init_return.py:40:5: ANN202 [*] Missing return type annotation for private function `__init__`
mypy_init_return.py:40:5: ANN202 Missing return type annotation for private function `__init__`
|
39 | # Error
40 | def __init__(self, foo: int):
| ^^^^^^^^ ANN202
41 | ...
|
= help: Add return type annotation: `None`
Unsafe fix
37 37 |
38 38 |
39 39 | # Error
40 |-def __init__(self, foo: int):
40 |+def __init__(self, foo: int) -> None:
41 41 | ...
42 42 |
43 43 |
= help: Add return type annotation
mypy_init_return.py:47:9: ANN204 [*] Missing return type annotation for special method `__init__`
|

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Parameters};
use ruff_python_ast::Parameters;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -51,9 +51,6 @@ impl Violation for NoReturnArgumentAnnotationInStub {
/// PYI050
pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &Parameters) {
// Ex) def func(arg: NoReturn): ...
// Ex) def func(arg: NoReturn, /): ...
// Ex) def func(*, arg: NoReturn): ...
for annotation in parameters
.posonlyargs
.iter()
@@ -61,36 +58,18 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &
.chain(&parameters.kwonlyargs)
.filter_map(|arg| arg.parameter.annotation.as_ref())
{
check_no_return_argument_annotation(checker, annotation);
}
// Ex) def func(*args: NoReturn): ...
if let Some(arg) = &parameters.vararg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
// Ex) def func(**kwargs: NoReturn): ...
if let Some(arg) = &parameters.kwarg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
}
fn check_no_return_argument_annotation(checker: &mut Checker, annotation: &Expr) {
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
},
},
},
annotation.range(),
));
annotation.range(),
));
}
}
}

View File

@@ -46,16 +46,12 @@ impl Violation for SnakeCaseTypeAlias {
///
/// ## Example
/// ```python
/// from typing import TypeAlias
///
/// _MyTypeT: TypeAlias = int
/// MyTypeT = int
/// ```
///
/// Use instead:
/// ```python
/// from typing import TypeAlias
///
/// _MyType: TypeAlias = int
/// MyType = int
/// ```
#[violation]
pub struct TSuffixedTypeAlias {

View File

@@ -29,12 +29,4 @@ PYI042.py:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
21 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
PYI042.py:27:6: PYI042 Type alias `foo_bar` should be CamelCase
|
26 | # PEP 695
27 | type foo_bar = int | str
| ^^^^^^^ PYI042
28 | type FooBar = int | str
|

View File

@@ -29,12 +29,4 @@ PYI042.pyi:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
21 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
PYI042.pyi:27:6: PYI042 Type alias `foo_bar` should be CamelCase
|
26 | # PEP 695
27 | type foo_bar = int | str
| ^^^^^^^ PYI042
28 | type FooBar = int | str
|

View File

@@ -30,12 +30,4 @@ PYI043.py:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffix
14 | ] # PYI043, since this ends in a T
|
PYI043.py:26:6: PYI043 Private type alias `_FooT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
25 | # PEP 695
26 | type _FooT = str | int
| ^^^^^ PYI043
27 | type Foo = str | int
|

View File

@@ -30,12 +30,4 @@ PYI043.pyi:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffi
14 | ] # PYI043, since this ends in a T
|
PYI043.pyi:26:6: PYI043 Private type alias `_FooT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
25 | # PEP 695
26 | type _FooT = str | int
| ^^^^^ PYI043
27 | type Foo = str | int
|

View File

@@ -28,67 +28,6 @@ PYI050.pyi:11:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument anno
11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
|
PYI050.pyi:13:21: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
|
PYI050.pyi:14:26: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
16 | def foo_int_args(*args: int): ...
|
PYI050.pyi:15:28: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
16 | def foo_int_args(*args: int): ...
17 | def foo_int_kwargs(**kwargs: int): ...
|
PYI050.pyi:15:48: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
16 | def foo_int_args(*args: int): ...
17 | def foo_int_kwargs(**kwargs: int): ...
|
PYI050.pyi:19:50: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
17 | def foo_int_kwargs(**kwargs: int): ...
18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ...
19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
21 | def foo_args_never(*args: Never): ...
|
PYI050.pyi:20:37: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ...
19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
| ^^^^^^^^ PYI050
21 | def foo_args_never(*args: Never): ...
22 | def foo_kwargs_never(**kwargs: Never): ...
|

View File

@@ -101,15 +101,11 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
return false;
};
if arguments.is_empty() {
return false;
}
// Match against, e.g., `apps.get_model("zerver", "Attachment")`.
if let Some(call_path) = collect_call_path(func.as_ref()) {
if matches!(call_path.as_slice(), [.., "get_model"]) {
if let Some(argument) =
arguments.find_argument("model_name", arguments.args.len().saturating_sub(1))
arguments.find_argument("model_name", arguments.args.len() - 1)
{
if let Some(string_literal) = argument.as_string_literal_expr() {
return string_literal.value.to_str() == name;

View File

@@ -38,20 +38,4 @@ N806.py:53:5: N806 Variable `Bad` in function should be lowercase
54 | ValidationError = import_string("django.core.exceptions.ValidationError") # OK
|
N806.py:56:5: N806 Variable `Bad` in function should be lowercase
|
54 | ValidationError = import_string("django.core.exceptions.ValidationError") # OK
55 |
56 | Bad = apps.get_model() # N806
| ^^^ N806
57 | Bad = apps.get_model(model_name="Stream") # N806
|
N806.py:57:5: N806 Variable `Bad` in function should be lowercase
|
56 | Bad = apps.get_model() # N806
57 | Bad = apps.get_model(model_name="Stream") # N806
| ^^^ N806
|

View File

@@ -31,6 +31,7 @@ mod tests {
#[test_case(Rule::BlankLineWithWhitespace, Path::new("W29.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_1.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_2.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501_3.py"))]
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]

View File

@@ -53,16 +53,24 @@ impl Overlong {
};
let mut chunks = line.split_whitespace();
let (Some(_), Some(second_chunk)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter.
return None;
};
if let (Some(_), Some(second_chunk)) = (chunks.next(), chunks.next()) {
// Do not enforce the line length for lines that end with a URL, as long as the URL
// begins before the limit.
let last_chunk = chunks.last().unwrap_or(second_chunk);
if last_chunk.contains("://") {
if width.get() - last_chunk.width() <= limit.value() as usize {
return None;
}
}
} else {
// This is a single word. If's a URL, ignore it.
if line.contains("://") {
return None;
}
// Do not enforce the line length for lines that end with a URL, as long as the URL
// begins before the limit.
let last_chunk = chunks.last().unwrap_or(second_chunk);
if last_chunk.contains("://") {
if width.get() - last_chunk.width() <= limit.value() as usize {
// This is a single word. If there are no split points, ignore it.
indexer.comment_ranges()
if !line.contains(['(', '[', ')', ']', '{', '}', ';']) {
return None;
}
}

View File

@@ -3,9 +3,9 @@ use memchr::memchr_iter;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::{StringKind, Tok};
use ruff_python_parser::Tok;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::fix::edits::pad_start;
@@ -58,6 +58,18 @@ impl AlwaysFixableViolation for InvalidEscapeSequence {
}
}
#[derive(Debug, PartialEq, Eq)]
enum FixTitle {
AddBackslash,
UseRawStringLiteral,
}
#[derive(Debug)]
struct InvalidEscapeChar {
ch: char,
range: TextRange,
}
/// W605
pub(crate) fn invalid_escape_sequence(
diagnostics: &mut Vec<Diagnostic>,
@@ -183,77 +195,41 @@ pub(crate) fn invalid_escape_sequence(
if contains_valid_escape_sequence {
// Escape with backslash.
for invalid_escape_char in &invalid_escape_chars {
let mut diagnostic = Diagnostic::new(
let diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::AddBackslash,
},
invalid_escape_char.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
invalid_escape_char.range,
)
.with_fix(Fix::safe_edit(Edit::insertion(
r"\".to_string(),
invalid_escape_char.start() + TextSize::from(1),
invalid_escape_char.range.start() + TextSize::from(1),
)));
invalid_escape_sequence.push(diagnostic);
}
} else {
// Turn into raw string.
for invalid_escape_char in &invalid_escape_chars {
let mut diagnostic = Diagnostic::new(
let diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::UseRawStringLiteral,
},
invalid_escape_char.range(),
);
if matches!(
token,
Tok::String {
kind: StringKind::Unicode,
..
}
) {
// Replace the Unicode prefix with `r`.
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
"r".to_string(),
invalid_escape_char.range,
)
.with_fix(
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), string_start_location, locator),
string_start_location,
string_start_location + TextSize::from(1),
)));
} else {
// Insert the `r` prefix.
diagnostic.set_fix(
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), string_start_location, locator),
string_start_location,
)),
);
}
)),
);
invalid_escape_sequence.push(diagnostic);
}
}
diagnostics.extend(invalid_escape_sequence);
}
#[derive(Debug, PartialEq, Eq)]
enum FixTitle {
AddBackslash,
UseRawStringLiteral,
}
#[derive(Debug)]
struct InvalidEscapeChar {
ch: char,
range: TextRange,
}
impl Ranged for InvalidEscapeChar {
fn range(&self) -> TextRange {
self.range
}
}

View File

@@ -125,8 +125,6 @@ W605_0.py:45:12: W605 [*] Invalid escape sequence: `\_`
44 |
45 | regex = '\\\_'
| ^^ W605
46 |
47 | #: W605:1:7
|
= help: Add backslash to escape sequence
@@ -136,23 +134,5 @@ W605_0.py:45:12: W605 [*] Invalid escape sequence: `\_`
44 44 |
45 |-regex = '\\\_'
45 |+regex = '\\\\_'
46 46 |
47 47 | #: W605:1:7
48 48 | u'foo\ bar'
W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
|
47 | #: W605:1:7
48 | u'foo\ bar'
| ^^ W605
|
= help: Use a raw string literal
Safe fix
45 45 | regex = '\\\_'
46 46 |
47 47 | #: W605:1:7
48 |-u'foo\ bar'
48 |+r'foo\ bar'

View File

@@ -1,227 +1,104 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.`
W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.`
|
3 | #: W605:1:10
4 | regex = f'\.png$'
| ^^ W605
5 |
6 | #: W605:2:1
1 | #: W605:1:10
2 | regex = '\.png$'
| ^^ W605
3 |
4 | #: W605:2:1
|
= help: Use a raw string literal
Safe fix
1 1 | # Same as `W605_0.py` but using f-strings instead.
2 2 |
3 3 | #: W605:1:10
4 |-regex = f'\.png$'
4 |+regex = rf'\.png$'
5 5 |
6 6 | #: W605:2:1
7 7 | regex = f'''
1 1 | #: W605:1:10
2 |-regex = '\.png$'
2 |+regex = r'\.png$'
3 3 |
4 4 | #: W605:2:1
5 5 | regex = '''
W605_1.py:8:1: W605 [*] Invalid escape sequence: `\.`
W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.`
|
6 | #: W605:2:1
7 | regex = f'''
8 | \.png$
4 | #: W605:2:1
5 | regex = '''
6 | \.png$
| ^^ W605
9 | '''
7 | '''
|
= help: Use a raw string literal
Safe fix
4 4 | regex = f'\.png$'
5 5 |
6 6 | #: W605:2:1
7 |-regex = f'''
7 |+regex = rf'''
8 8 | \.png$
9 9 | '''
10 10 |
2 2 | regex = '\.png$'
3 3 |
4 4 | #: W605:2:1
5 |-regex = '''
5 |+regex = r'''
6 6 | \.png$
7 7 | '''
8 8 |
W605_1.py:13:7: W605 [*] Invalid escape sequence: `\_`
W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_`
|
11 | #: W605:2:6
12 | f(
13 | f'\_'
| ^^ W605
14 | )
|
= help: Use a raw string literal
Safe fix
10 10 |
11 11 | #: W605:2:6
12 12 | f(
13 |- f'\_'
13 |+ rf'\_'
14 14 | )
15 15 |
16 16 | #: W605:4:6
W605_1.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
18 | multi-line
19 | literal
20 | with \_ somewhere
9 | #: W605:2:6
10 | f(
11 | '\_'
| ^^ W605
21 | in the middle
22 | """
12 | )
|
= help: Use a raw string literal
Safe fix
14 14 | )
15 15 |
16 16 | #: W605:4:6
17 |-f"""
17 |+rf"""
18 18 | multi-line
19 19 | literal
20 20 | with \_ somewhere
8 8 |
9 9 | #: W605:2:6
10 10 | f(
11 |- '\_'
11 |+ r'\_'
12 12 | )
13 13 |
14 14 | #: W605:4:6
W605_1.py:25:40: W605 [*] Invalid escape sequence: `\_`
W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_`
|
24 | #: W605:1:38
25 | value = f'new line\nand invalid escape \_ here'
| ^^ W605
16 | multi-line
17 | literal
18 | with \_ somewhere
| ^^ W605
19 | in the middle
20 | """
|
= help: Add backslash to escape sequence
= help: Use a raw string literal
Safe fix
22 22 | """
23 23 |
24 24 | #: W605:1:38
25 |-value = f'new line\nand invalid escape \_ here'
25 |+value = f'new line\nand invalid escape \\_ here'
12 12 | )
13 13 |
14 14 | #: W605:4:6
15 |-"""
15 |+r"""
16 16 | multi-line
17 17 | literal
18 18 | with \_ somewhere
W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.`
|
23 | def f():
24 | #: W605:1:11
25 | return'\.png$'
| ^^ W605
26 |
27 | #: Okay
|
= help: Use a raw string literal
Safe fix
22 22 |
23 23 | def f():
24 24 | #: W605:1:11
25 |- return'\.png$'
25 |+ return r'\.png$'
26 26 |
27 27 |
28 28 | #: Okay
W605_1.py:43:13: W605 [*] Invalid escape sequence: `\_`
|
41 | ''' # noqa
42 |
43 | regex = f'\\\_'
| ^^ W605
44 | value = f'\{{1}}'
45 | value = f'\{1}'
|
= help: Add backslash to escape sequence
Safe fix
40 40 | \w
41 41 | ''' # noqa
42 42 |
43 |-regex = f'\\\_'
43 |+regex = f'\\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
W605_1.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
| ^^ W605
45 | value = f'\{1}'
46 | value = f'{1:\}'
|
= help: Use a raw string literal
Safe fix
41 41 | ''' # noqa
42 42 |
43 43 | regex = f'\\\_'
44 |-value = f'\{{1}}'
44 |+value = rf'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
W605_1.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
45 | value = f'\{1}'
| ^^ W605
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
42 42 |
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 |-value = f'\{1}'
45 |+value = rf'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
W605_1.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
44 | value = f'\{{1}}'
45 | value = f'\{1}'
46 | value = f'{1:\}'
| ^^ W605
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 |-value = f'{1:\}'
46 |+value = rf'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
W605_1.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
45 | value = f'\{1}'
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
| ^^ W605
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 |-value = f"{f"\{1}"}"
47 |+value = f"{rf"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
50 50 | # Okay
W605_1.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
| ^^ W605
49 |
50 | # Okay
|
= help: Use a raw string literal
Safe fix
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 |-value = rf"{f"\{1}"}"
48 |+value = rf"{rf"\{1}"}"
49 49 |
50 50 | # Okay
51 51 | value = rf'\{{1}}'
27 27 | #: Okay
28 28 | regex = r'\.png$'

View File

@@ -0,0 +1,227 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
3 | #: W605:1:10
4 | regex = f'\.png$'
| ^^ W605
5 |
6 | #: W605:2:1
|
= help: Use a raw string literal
Safe fix
1 1 | # Same as `W605_0.py` but using f-strings instead.
2 2 |
3 3 | #: W605:1:10
4 |-regex = f'\.png$'
4 |+regex = rf'\.png$'
5 5 |
6 6 | #: W605:2:1
7 7 | regex = f'''
W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.`
|
6 | #: W605:2:1
7 | regex = f'''
8 | \.png$
| ^^ W605
9 | '''
|
= help: Use a raw string literal
Safe fix
4 4 | regex = f'\.png$'
5 5 |
6 6 | #: W605:2:1
7 |-regex = f'''
7 |+regex = rf'''
8 8 | \.png$
9 9 | '''
10 10 |
W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_`
|
11 | #: W605:2:6
12 | f(
13 | f'\_'
| ^^ W605
14 | )
|
= help: Use a raw string literal
Safe fix
10 10 |
11 11 | #: W605:2:6
12 12 | f(
13 |- f'\_'
13 |+ rf'\_'
14 14 | )
15 15 |
16 16 | #: W605:4:6
W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
18 | multi-line
19 | literal
20 | with \_ somewhere
| ^^ W605
21 | in the middle
22 | """
|
= help: Use a raw string literal
Safe fix
14 14 | )
15 15 |
16 16 | #: W605:4:6
17 |-f"""
17 |+rf"""
18 18 | multi-line
19 19 | literal
20 20 | with \_ somewhere
W605_2.py:25:40: W605 [*] Invalid escape sequence: `\_`
|
24 | #: W605:1:38
25 | value = f'new line\nand invalid escape \_ here'
| ^^ W605
|
= help: Add backslash to escape sequence
Safe fix
22 22 | """
23 23 |
24 24 | #: W605:1:38
25 |-value = f'new line\nand invalid escape \_ here'
25 |+value = f'new line\nand invalid escape \\_ here'
26 26 |
27 27 |
28 28 | #: Okay
W605_2.py:43:13: W605 [*] Invalid escape sequence: `\_`
|
41 | ''' # noqa
42 |
43 | regex = f'\\\_'
| ^^ W605
44 | value = f'\{{1}}'
45 | value = f'\{1}'
|
= help: Add backslash to escape sequence
Safe fix
40 40 | \w
41 41 | ''' # noqa
42 42 |
43 |-regex = f'\\\_'
43 |+regex = f'\\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
| ^^ W605
45 | value = f'\{1}'
46 | value = f'{1:\}'
|
= help: Use a raw string literal
Safe fix
41 41 | ''' # noqa
42 42 |
43 43 | regex = f'\\\_'
44 |-value = f'\{{1}}'
44 |+value = rf'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
45 | value = f'\{1}'
| ^^ W605
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
42 42 |
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 |-value = f'\{1}'
45 |+value = rf'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
44 | value = f'\{{1}}'
45 | value = f'\{1}'
46 | value = f'{1:\}'
| ^^ W605
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 |-value = f'{1:\}'
46 |+value = rf'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
45 | value = f'\{1}'
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
| ^^ W605
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 |-value = f"{f"\{1}"}"
47 |+value = f"{rf"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
50 50 | # Okay
W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
| ^^ W605
49 |
50 | # Okay
|
= help: Use a raw string literal
Safe fix
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 |-value = rf"{f"\{1}"}"
48 |+value = rf"{rf"\{1}"}"
49 49 |
50 50 | # Okay
51 51 | value = rf'\{{1}}'

View File

@@ -79,6 +79,15 @@ E501_2.py:14:6: E501 Line too long (7 > 6)
16 | [1, 2]
|
E501_2.py:15:6: E501 Line too long (7 > 6)
|
13 | [12]
14 | [12 ]
15 | [1,2]
| ^ E501
16 | [1, 2]
|
E501_2.py:16:6: E501 Line too long (8 > 6)
|
14 | [12 ]

View File

@@ -89,6 +89,15 @@ E501_2.py:10:3: E501 Line too long (13 > 6)
12 | if True: # noqa: E501
|
E501_2.py:13:4: E501 Line too long (8 > 6)
|
12 | if True: # noqa: E501
13 | [12]
| ^^ E501
14 | [12 ]
15 | [1,2]
|
E501_2.py:14:4: E501 Line too long (9 > 6)
|
12 | if True: # noqa: E501
@@ -99,6 +108,15 @@ E501_2.py:14:4: E501 Line too long (9 > 6)
16 | [1, 2]
|
E501_2.py:15:4: E501 Line too long (9 > 6)
|
13 | [12]
14 | [12 ]
15 | [1,2]
| ^^^ E501
16 | [1, 2]
|
E501_2.py:16:4: E501 Line too long (10 > 6)
|
14 | [12 ]

View File

@@ -89,6 +89,15 @@ E501_2.py:10:2: E501 Line too long (21 > 6)
12 | if True: # noqa: E501
|
E501_2.py:13:2: E501 Line too long (12 > 6)
|
12 | if True: # noqa: E501
13 | [12]
| ^^^^ E501
14 | [12 ]
15 | [1,2]
|
E501_2.py:14:2: E501 Line too long (13 > 6)
|
12 | if True: # noqa: E501
@@ -99,6 +108,15 @@ E501_2.py:14:2: E501 Line too long (13 > 6)
16 | [1, 2]
|
E501_2.py:15:2: E501 Line too long (13 > 6)
|
13 | [12]
14 | [12 ]
15 | [1,2]
| ^^^^^ E501
16 | [1, 2]
|
E501_2.py:16:2: E501 Line too long (14 > 6)
|
14 | [12 ]

View File

@@ -172,9 +172,8 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
let mut has_seen_tab = docstring.indentation.contains('\t');
let mut is_over_indented = true;
let mut over_indented_lines = vec![];
let mut over_indented_size = usize::MAX;
let mut over_indented_offset = usize::MAX;
let docstring_indent_size = docstring.indentation.chars().count();
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
@@ -190,7 +189,6 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
}
let line_indent = leading_space(line);
let line_indent_size = line_indent.chars().count();
// We only report tab indentation once, so only check if we haven't seen a tab
// yet.
@@ -199,7 +197,9 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
if checker.enabled(Rule::UnderIndentation) {
// We report under-indentation on every line. This isn't great, but enables
// fix.
if (i == lines.len() - 1 || !is_blank) && line_indent_size < docstring_indent_size {
if (i == lines.len() - 1 || !is_blank)
&& line_indent.len() < docstring.indentation.len()
{
let mut diagnostic =
Diagnostic::new(UnderIndentation, TextRange::empty(line.start()));
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
@@ -217,12 +217,14 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// until we've viewed all the lines, so for now, just track
// the over-indentation status of every line.
if i < lines.len() - 1 {
if line_indent_size > docstring_indent_size {
if line_indent.len() > docstring.indentation.len() {
over_indented_lines.push(line);
// Track the _smallest_ offset we see, in terms of characters.
over_indented_size =
std::cmp::min(line_indent_size - docstring_indent_size, over_indented_size);
over_indented_offset = std::cmp::min(
line_indent.chars().count() - docstring.indentation.chars().count(),
over_indented_offset,
);
} else {
is_over_indented = false;
}
@@ -248,21 +250,21 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// enables the fix capability.
let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(line.start()));
let edit = if indent.is_empty() {
// Delete the entire indent.
Edit::range_deletion(TextRange::at(line.start(), line_indent.text_len()))
Edit::deletion(line.start(), line_indent.text_len())
} else {
// Convert the character count to an offset within the source.
let offset = checker
.locator()
.after(line.start() + indent.text_len())
.chars()
.take(over_indented_size)
.take(over_indented_offset)
.map(TextLen::text_len)
.sum::<TextSize>();
let range = TextRange::at(line.start(), indent.text_len() + offset);
Edit::range_replacement(indent, range)
Edit::range_replacement(
indent.clone(),
TextRange::at(line.start(), indent.text_len() + offset),
)
};
diagnostic.set_fix(Fix::safe_edit(edit));
checker.diagnostics.push(diagnostic);
@@ -272,8 +274,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// If the last line is over-indented...
if let Some(last) = lines.last() {
let line_indent = leading_space(last);
let line_indent_size = line_indent.chars().count();
if line_indent_size > docstring_indent_size {
if line_indent.len() > docstring.indentation.len() {
let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(last.start()));
let indent = clean_space(docstring.indentation);

View File

@@ -1,81 +1,57 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D208.py:2:1: D208 [*] Docstring is over-indented
D208.py:3:1: D208 [*] Docstring is over-indented
|
1 | """
2 | Author
1 | class Platform:
2 | """ Remove sampler
3 | Args:
| D208
3 | """
4 |     Returns:
5 | """
|
= help: Remove over-indentation
Safe fix
1 1 | """
2 |- Author
2 |+Author
3 3 | """
4 4 |
5 5 |
1 1 | class Platform:
2 2 | """ Remove sampler
3 |- Args:
3 |+ Args:
4 4 |     Returns:
5 5 | """
D208.py:8:1: D208 [*] Docstring is over-indented
|
6 | class Platform:
7 | """ Remove sampler
8 | Args:
| D208
9 |     Returns:
10 | """
|
= help: Remove over-indentation
D208.py:4:1: D208 [*] Docstring is over-indented
|
2 | """ Remove sampler
3 | Args:
4 |     Returns:
| D208
5 | """
|
= help: Remove over-indentation
Safe fix
5 5 |
6 6 | class Platform:
7 7 | """ Remove sampler
8 |- Args:
8 |+ Args:
9 9 |     Returns:
10 10 | """
11 11 |
1 1 | class Platform:
2 2 | """ Remove sampler
3 3 | Args:
4 |-     Returns:
4 |+ Returns:
5 5 | """
D208.py:9:1: D208 [*] Docstring is over-indented
|
7 | """ Remove sampler
8 | Args:
9 |     Returns:
| D208
10 | """
|
= help: Remove over-indentation
D208.py:5:1: D208 [*] Docstring is over-indented
|
3 | Args:
4 |     Returns:
5 | """
| D208
|
= help: Remove over-indentation
Safe fix
6 6 | class Platform:
7 7 | """ Remove sampler
8 8 | Args:
9 |-     Returns:
9 |+ Returns:
10 10 | """
11 11 |
12 12 |
D208.py:10:1: D208 [*] Docstring is over-indented
|
8 | Args:
9 |     Returns:
10 | """
| D208
|
= help: Remove over-indentation
Safe fix
7 7 | """ Remove sampler
8 8 | Args:
9 9 |     Returns:
10 |- """
10 |+ """
11 11 |
12 12 |
13 13 | def memory_test():
2 2 | """ Remove sampler
3 3 | Args:
4 4 |     Returns:
5 |- """
5 |+ """

View File

@@ -1,11 +1,9 @@
use std::fmt;
use ruff_python_ast as ast;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{visitor, Arguments, CmpOp, Expr, Stmt};
use ruff_python_ast::{Arguments, CmpOp, Expr};
use ruff_python_semantic::analyze::function_type;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_text_size::TextRange;
use crate::settings::LinterSettings;
@@ -84,116 +82,3 @@ impl fmt::Display for CmpOpExt {
write!(f, "{representation}")
}
}
/// Visitor to track reads from an iterable in a loop.
#[derive(Debug)]
pub(crate) struct SequenceIndexVisitor<'a> {
/// `letters`, given `for index, letter in enumerate(letters)`.
sequence_name: &'a str,
/// `index`, given `for index, letter in enumerate(letters)`.
index_name: &'a str,
/// `letter`, given `for index, letter in enumerate(letters)`.
value_name: &'a str,
/// The ranges of any `letters[index]` accesses.
accesses: Vec<TextRange>,
/// Whether any of the variables have been modified.
modified: bool,
}
impl<'a> SequenceIndexVisitor<'a> {
pub(crate) fn new(sequence_name: &'a str, index_name: &'a str, value_name: &'a str) -> Self {
Self {
sequence_name,
index_name,
value_name,
accesses: Vec::new(),
modified: false,
}
}
pub(crate) fn into_accesses(self) -> Vec<TextRange> {
self.accesses
}
}
impl SequenceIndexVisitor<'_> {
fn is_assignment(&self, expr: &Expr) -> bool {
// If we see the sequence, a subscript, or the index being modified, we'll stop emitting
// diagnostics.
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
id == self.sequence_name || id == self.index_name || id == self.value_name
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return false;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return false;
};
if id == self.index_name {
return true;
}
}
false
}
_ => false,
}
}
}
impl<'a> Visitor<'_> for SequenceIndexVisitor<'a> {
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.modified {
return;
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
self.visit_expr(value);
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if let Some(value) = value {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
Stmt::Delete(ast::StmtDelete { targets, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
}
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if self.modified {
return;
}
match expr {
Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return;
};
if id == self.index_name {
self.accesses.push(*range);
}
}
}
_ => visitor::walk_expr(self, expr),
}
}
}

View File

@@ -94,7 +94,6 @@ mod tests {
#[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"))]
#[test_case(Rule::ReturnInInit, Path::new("return_in_init.py"))]
#[test_case(Rule::TooManyArguments, Path::new("too_many_arguments.py"))]
#[test_case(Rule::TooManyPositional, Path::new("too_many_positional.py"))]
#[test_case(Rule::TooManyBranches, Path::new("too_many_branches.py"))]
#[test_case(
Rule::TooManyReturnStatements,
@@ -161,10 +160,6 @@ mod tests {
)]
#[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(
Rule::UnnecessaryDictIndexLookup,
Path::new("unnecessary_dict_index_lookup.py")
)]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -250,22 +245,6 @@ mod tests {
Ok(())
}
#[test]
fn max_positional_args() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/too_many_positional_params.py"),
&LinterSettings {
pylint: pylint::settings::Settings {
max_positional_args: 4,
..pylint::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::TooManyPositional)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn max_branches() -> Result<()> {
let diagnostics = test_path(
@@ -345,15 +324,4 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn unspecified_encoding_python39_or_lower() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/unspecified_encoding.py"),
&LinterSettings::for_rule(Rule::UnspecifiedEncoding)
.with_target_version(PythonVersion::Py39),
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -55,7 +55,6 @@ pub(crate) use sys_exit_alias::*;
pub(crate) use too_many_arguments::*;
pub(crate) use too_many_boolean_expressions::*;
pub(crate) use too_many_branches::*;
pub(crate) use too_many_positional::*;
pub(crate) use too_many_public_methods::*;
pub(crate) use too_many_return_statements::*;
pub(crate) use too_many_statements::*;
@@ -63,7 +62,6 @@ pub(crate) use type_bivariance::*;
pub(crate) use type_name_incorrect_variance::*;
pub(crate) use type_param_name_mismatch::*;
pub(crate) use unexpected_special_method_signature::*;
pub(crate) use unnecessary_dict_index_lookup::*;
pub(crate) use unnecessary_direct_lambda_call::*;
pub(crate) use unnecessary_lambda::*;
pub(crate) use unnecessary_list_index_lookup::*;
@@ -132,7 +130,6 @@ mod sys_exit_alias;
mod too_many_arguments;
mod too_many_boolean_expressions;
mod too_many_branches;
mod too_many_positional;
mod too_many_public_methods;
mod too_many_return_statements;
mod too_many_statements;
@@ -140,7 +137,6 @@ mod type_bivariance;
mod type_name_incorrect_variance;
mod type_param_name_mismatch;
mod unexpected_special_method_signature;
mod unnecessary_dict_index_lookup;
mod unnecessary_direct_lambda_call;
mod unnecessary_lambda;
mod unnecessary_list_index_lookup;

View File

@@ -1,8 +1,8 @@
use ruff_python_ast::{Parameters, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::visibility;
use crate::checkers::ast::Checker;
@@ -58,13 +58,12 @@ impl Violation for TooManyArguments {
}
/// PLR0913
pub(crate) fn too_many_arguments(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
let num_arguments = function_def
.parameters
pub(crate) fn too_many_arguments(checker: &mut Checker, parameters: &Parameters, stmt: &Stmt) {
let num_arguments = parameters
.args
.iter()
.chain(&function_def.parameters.kwonlyargs)
.chain(&function_def.parameters.posonlyargs)
.chain(&parameters.kwonlyargs)
.chain(&parameters.posonlyargs)
.filter(|arg| {
!checker
.settings
@@ -72,22 +71,13 @@ pub(crate) fn too_many_arguments(checker: &mut Checker, function_def: &ast::Stmt
.is_match(&arg.parameter.name)
})
.count();
if num_arguments > checker.settings.pylint.max_args {
// Allow excessive arguments in `@override` or `@overload` methods, since they're required
// to adhere to the parent signature.
if visibility::is_override(&function_def.decorator_list, checker.semantic())
|| visibility::is_overload(&function_def.decorator_list, checker.semantic())
{
return;
}
checker.diagnostics.push(Diagnostic::new(
TooManyArguments {
c_args: num_arguments,
max_args: checker.settings.pylint.max_args,
},
function_def.identifier(),
stmt.identifier(),
));
}
}

View File

@@ -1,79 +0,0 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{identifier::Identifier, Parameters, Stmt};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function definitions that include too many positional arguments.
///
/// By default, this rule allows up to five arguments, as configured by the
/// [`pylint.max-positional-args`] option.
///
/// ## Why is this bad?
/// Functions with many arguments are harder to understand, maintain, and call.
/// This is especially true for functions with many positional arguments, as
/// providing arguments positionally is more error-prone and less clear to
/// readers than providing arguments by name.
///
/// Consider refactoring functions with many arguments into smaller functions
/// with fewer arguments, using objects to group related arguments, or
/// migrating to keyword-only arguments.
///
/// ## Example
/// ```python
/// def plot(x, y, z, color, mark, add_trendline):
/// ...
///
///
/// plot(1, 2, 3, "r", "*", True)
/// ```
///
/// Use instead:
/// ```python
/// def plot(x, y, z, *, color, mark, add_trendline):
/// ...
///
///
/// plot(1, 2, 3, color="r", mark="*", add_trendline=True)
/// ```
///
/// ## Options
/// - `pylint.max-positional-args`
#[violation]
pub struct TooManyPositional {
c_pos: usize,
max_pos: usize,
}
impl Violation for TooManyPositional {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyPositional { c_pos, max_pos } = self;
format!("Too many positional arguments: ({c_pos}/{max_pos})")
}
}
/// PLR0917
pub(crate) fn too_many_positional(checker: &mut Checker, parameters: &Parameters, stmt: &Stmt) {
let num_positional_args = parameters
.args
.iter()
.chain(&parameters.posonlyargs)
.filter(|arg| {
!checker
.settings
.dummy_variable_rgx
.is_match(&arg.parameter.name)
})
.count();
if num_positional_args > checker.settings.pylint.max_positional_args {
checker.diagnostics.push(Diagnostic::new(
TooManyPositional {
c_pos: num_positional_args,
max_pos: checker.settings.pylint.max_positional_args,
},
stmt.identifier(),
));
}
}

View File

@@ -1,160 +0,0 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, StmtFor};
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::SequenceIndexVisitor;
/// ## What it does
/// Checks for key-based dict accesses during `.items()` iterations.
///
/// ## Why is this bad?
/// When iterating over a dict via `.items()`, the current value is already
/// available alongside its key. Using the key to look up the value is
/// unnecessary.
///
/// ## Example
/// ```python
/// FRUITS = {"apple": 1, "orange": 10, "berry": 22}
///
/// for fruit_name, fruit_count in FRUITS.items():
/// print(FRUITS[fruit_name])
/// ```
///
/// Use instead:
/// ```python
/// FRUITS = {"apple": 1, "orange": 10, "berry": 22}
///
/// for fruit_name, fruit_count in FRUITS.items():
/// print(fruit_count)
/// ```
#[violation]
pub struct UnnecessaryDictIndexLookup;
impl AlwaysFixableViolation for UnnecessaryDictIndexLookup {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary lookup of dictionary value by key")
}
fn fix_title(&self) -> String {
format!("Use existing variable")
}
}
/// PLR1733
pub(crate) fn unnecessary_dict_index_lookup(checker: &mut Checker, stmt_for: &StmtFor) {
let Some((dict_name, index_name, value_name)) = dict_items(&stmt_for.iter, &stmt_for.target)
else {
return;
};
let ranges = {
let mut visitor = SequenceIndexVisitor::new(dict_name, index_name, value_name);
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.to_string(),
range,
)));
checker.diagnostics.push(diagnostic);
}
}
/// PLR1733
pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &mut Checker, expr: &Expr) {
let (Expr::GeneratorExp(ast::ExprGeneratorExp {
elt, generators, ..
})
| Expr::DictComp(ast::ExprDictComp {
value: elt,
generators,
..
})
| Expr::SetComp(ast::ExprSetComp {
elt, generators, ..
})
| Expr::ListComp(ast::ExprListComp {
elt, generators, ..
})) = expr
else {
return;
};
for comp in generators {
let Some((dict_name, index_name, value_name)) = dict_items(&comp.iter, &comp.target) else {
continue;
};
let ranges = {
let mut visitor = SequenceIndexVisitor::new(dict_name, index_name, value_name);
visitor.visit_expr(elt.as_ref());
for expr in &comp.ifs {
visitor.visit_expr(expr);
}
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.to_string(),
range,
)));
checker.diagnostics.push(diagnostic);
}
}
}
fn dict_items<'a>(
call_expr: &'a Expr,
tuple_expr: &'a Expr,
) -> Option<(&'a str, &'a str, &'a str)> {
let ast::ExprCall {
func, arguments, ..
} = call_expr.as_call_expr()?;
if !arguments.is_empty() {
return None;
}
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
return None;
};
if attr != "items" {
return None;
}
let Expr::Name(ast::ExprName { id: dict_name, .. }) = value.as_ref() else {
return None;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = tuple_expr else {
return None;
};
let [index, value] = elts.as_slice() else {
return None;
};
// Grab the variable names.
let Expr::Name(ast::ExprName { id: index_name, .. }) = index else {
return None;
};
let Expr::Name(ast::ExprName { id: value_name, .. }) = value else {
return None;
};
// If either of the variable names are intentionally ignored by naming them `_`, then don't
// emit.
if index_name == "_" || value_name == "_" {
return None;
}
Some((dict_name, index_name, value_name))
}

View File

@@ -1,11 +1,13 @@
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFor};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, StmtFor};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::SequenceIndexVisitor;
/// ## What it does
/// Checks for index-based list accesses during `enumerate` iterations.
@@ -53,10 +55,10 @@ pub(crate) fn unnecessary_list_index_lookup(checker: &mut Checker, stmt_for: &St
};
let ranges = {
let mut visitor = SequenceIndexVisitor::new(sequence, index_name, value_name);
let mut visitor = SubscriptVisitor::new(sequence, index_name);
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);
visitor.into_accesses()
visitor.diagnostic_ranges
};
for range in ranges {
@@ -97,9 +99,9 @@ pub(crate) fn unnecessary_list_index_lookup_comprehension(checker: &mut Checker,
};
let ranges = {
let mut visitor = SequenceIndexVisitor::new(sequence, index_name, value_name);
let mut visitor = SubscriptVisitor::new(sequence, index_name);
visitor.visit_expr(elt.as_ref());
visitor.into_accesses()
visitor.diagnostic_ranges
};
for range in ranges {
@@ -159,3 +161,104 @@ fn enumerate_items<'a>(
Some((sequence, index_name, value_name))
}
#[derive(Debug)]
struct SubscriptVisitor<'a> {
sequence_name: &'a str,
index_name: &'a str,
diagnostic_ranges: Vec<TextRange>,
modified: bool,
}
impl<'a> SubscriptVisitor<'a> {
fn new(sequence_name: &'a str, index_name: &'a str) -> Self {
Self {
sequence_name,
index_name,
diagnostic_ranges: Vec::new(),
modified: false,
}
}
}
impl SubscriptVisitor<'_> {
fn is_assignment(&self, expr: &Expr) -> bool {
// If we see the sequence, a subscript, or the index being modified, we'll stop emitting
// diagnostics.
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
id == self.sequence_name || id == self.index_name
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return false;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return false;
};
if id == self.index_name {
return true;
}
}
false
}
_ => false,
}
}
}
impl<'a> Visitor<'_> for SubscriptVisitor<'a> {
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.modified {
return;
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
self.visit_expr(value);
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if let Some(value) = value {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
Stmt::Delete(ast::StmtDelete { targets, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
}
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if self.modified {
return;
}
match expr {
Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return;
};
if id == self.index_name {
self.diagnostic_ranges.push(*range);
}
}
}
_ => visitor::walk_expr(self, expr),
}
}
}

View File

@@ -1,16 +1,10 @@
use anyhow::Result;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::call_path::{format_call_path, CallPath};
use ruff_python_ast::Expr;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::importer::ImportRequest;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for uses of `open` and related calls without an explicit `encoding`
@@ -21,9 +15,7 @@ use crate::settings::types::PythonVersion;
/// non-portable code, with differing behavior across platforms.
///
/// Instead, consider using the `encoding` parameter to enforce a specific
/// encoding. [PEP 597] recommends using `locale.getpreferredencoding(False)`
/// as the default encoding on versions earlier than Python 3.10, and
/// `encoding="locale"` on Python 3.10 and later.
/// encoding.
///
/// ## Example
/// ```python
@@ -37,15 +29,13 @@ use crate::settings::types::PythonVersion;
///
/// ## References
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
///
/// [PEP 597]: https://peps.python.org/pep-0597/
#[violation]
pub struct UnspecifiedEncoding {
function_name: String,
mode: Mode,
}
impl AlwaysFixableViolation for UnspecifiedEncoding {
impl Violation for UnspecifiedEncoding {
#[derive_message_formats]
fn message(&self) -> String {
let UnspecifiedEncoding {
@@ -62,10 +52,6 @@ impl AlwaysFixableViolation for UnspecifiedEncoding {
}
}
}
fn fix_title(&self) -> String {
format!("Add explicit `encoding` argument")
}
}
/// PLW1514
@@ -84,63 +70,17 @@ pub(crate) fn unspecified_encoding(checker: &mut Checker, call: &ast::ExprCall)
return;
};
let mut diagnostic = Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
UnspecifiedEncoding {
function_name,
mode,
},
call.func.range(),
);
if checker.settings.target_version >= PythonVersion::Py310 {
diagnostic.set_fix(generate_keyword_fix(checker, call));
} else {
diagnostic.try_set_fix(|| generate_import_fix(checker, call));
}
checker.diagnostics.push(diagnostic);
}
/// Generate an [`Edit`] for Python 3.10 and later.
fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
Fix::unsafe_edit(add_argument(
&format!(
"encoding={}",
checker
.generator()
.expr(&Expr::StringLiteral(ast::ExprStringLiteral {
value: ast::StringLiteralValue::single(ast::StringLiteral {
value: "locale".to_string(),
unicode: false,
range: TextRange::default(),
}),
range: TextRange::default(),
}))
),
&call.arguments,
checker.indexer().comment_ranges(),
checker.locator().contents(),
))
}
/// Generate an [`Edit`] for Python 3.9 and earlier.
fn generate_import_fix(checker: &Checker, call: &ast::ExprCall) -> Result<Fix> {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("locale", "getpreferredencoding"),
call.start(),
checker.semantic(),
)?;
let argument_edit = add_argument(
&format!("encoding={binding}(False)"),
&call.arguments,
checker.indexer().comment_ranges(),
checker.locator().contents(),
);
Ok(Fix::unsafe_edits(import_edit, [argument_edit]))
));
}
/// Returns `true` if the given expression is a string literal containing a `b` character.
fn is_binary_mode(expr: &Expr) -> Option<bool> {
fn is_binary_mode(expr: &ast::Expr) -> Option<bool> {
Some(
expr.as_string_literal_expr()?
.value
@@ -152,7 +92,12 @@ fn is_binary_mode(expr: &Expr) -> Option<bool> {
/// Returns `true` if the given call lacks an explicit `encoding`.
fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool {
// If we have something like `*args`, which might contain the encoding argument, abort.
if call.arguments.args.iter().any(Expr::is_starred_expr) {
if call
.arguments
.args
.iter()
.any(ruff_python_ast::Expr::is_starred_expr)
{
return false;
}
// If we have something like `**kwargs`, which might contain the encoding argument, abort.

View File

@@ -39,7 +39,6 @@ pub struct Settings {
pub allow_magic_value_types: Vec<ConstantType>,
pub allow_dunder_method_names: FxHashSet<String>,
pub max_args: usize,
pub max_positional_args: usize,
pub max_returns: usize,
pub max_bool_expr: usize,
pub max_branches: usize,
@@ -53,7 +52,6 @@ impl Default for Settings {
allow_magic_value_types: vec![ConstantType::Str, ConstantType::Bytes],
allow_dunder_method_names: FxHashSet::default(),
max_args: 5,
max_positional_args: 5,
max_returns: 6,
max_bool_expr: 5,
max_branches: 12,

View File

@@ -1,25 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_positional.py:1:5: PLR0917 Too many positional arguments: (8/5)
|
1 | def f(x, y, z, t, u, v, w, r): # Too many positional arguments (8/3)
| ^ PLR0917
2 | pass
|
too_many_positional.py:21:5: PLR0917 Too many positional arguments: (6/5)
|
21 | def f(x, y, z, /, u, v, w): # Too many positional arguments (6/3)
| ^ PLR0917
22 | pass
|
too_many_positional.py:29:5: PLR0917 Too many positional arguments: (6/5)
|
29 | def f(x, y, z, a, b, c, *, u, v, w): # Too many positional arguments (6/3)
| ^ PLR0917
30 | pass
|

View File

@@ -1,124 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unnecessary_dict_index_lookup.py:4:6: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
3 | def fix_these():
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
|
= help: Use existing variable
Safe fix
1 1 | FRUITS = {"apple": 1, "orange": 10, "berry": 22}
2 2 |
3 3 | def fix_these():
4 |- [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
4 |+ [fruit_count for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
unnecessary_dict_index_lookup.py:5:6: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
3 | def fix_these():
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
|
= help: Use existing variable
Safe fix
2 2 |
3 3 | def fix_these():
4 4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 |- {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
5 |+ {fruit_count for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
unnecessary_dict_index_lookup.py:6:18: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
7 |
8 | for fruit_name, fruit_count in FRUITS.items():
|
= help: Use existing variable
Safe fix
3 3 | def fix_these():
4 4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 |- {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 |+ {fruit_name: fruit_count for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
unnecessary_dict_index_lookup.py:9:15: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
8 | for fruit_name, fruit_count in FRUITS.items():
9 | print(FRUITS[fruit_name]) # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
|
= help: Use existing variable
Safe fix
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 |- print(FRUITS[fruit_name]) # PLR1733
9 |+ print(fruit_count) # PLR1733
10 10 | blah = FRUITS[fruit_name] # PLR1733
11 11 | assert FRUITS[fruit_name] == "pear" # PLR1733
12 12 |
unnecessary_dict_index_lookup.py:10:16: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
8 | for fruit_name, fruit_count in FRUITS.items():
9 | print(FRUITS[fruit_name]) # PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
|
= help: Use existing variable
Safe fix
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
10 |- blah = FRUITS[fruit_name] # PLR1733
10 |+ blah = fruit_count # PLR1733
11 11 | assert FRUITS[fruit_name] == "pear" # PLR1733
12 12 |
13 13 |
unnecessary_dict_index_lookup.py:11:16: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
9 | print(FRUITS[fruit_name]) # PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
|
= help: Use existing variable
Safe fix
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
10 10 | blah = FRUITS[fruit_name] # PLR1733
11 |- assert FRUITS[fruit_name] == "pear" # PLR1733
11 |+ assert fruit_count == "pear" # PLR1733
12 12 |
13 13 |
14 14 | def dont_fix_these():

View File

@@ -80,7 +80,7 @@ unnecessary_list_index_lookup.py:12:15: PLR1736 [*] Unnecessary lookup of list i
12 |+ print(letter) # PLR1736
13 13 | blah = letters[index] # PLR1736
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
unnecessary_list_index_lookup.py:13:16: PLR1736 [*] Unnecessary lookup of list item by index
|
@@ -99,7 +99,7 @@ unnecessary_list_index_lookup.py:13:16: PLR1736 [*] Unnecessary lookup of list i
13 |- blah = letters[index] # PLR1736
13 |+ blah = letter # PLR1736
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list item by index
@@ -108,7 +108,7 @@ unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list i
13 | blah = letters[index] # PLR1736
14 | assert letters[index] == "d" # PLR1736
| ^^^^^^^^^^^^^^ PLR1736
15 |
15 |
16 | for index, letter in builtins.enumerate(letters):
|
= help: Use existing variable
@@ -119,7 +119,7 @@ unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list i
13 13 | blah = letters[index] # PLR1736
14 |- assert letters[index] == "d" # PLR1736
14 |+ assert letter == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 17 | print(letters[index]) # PLR1736
@@ -135,7 +135,7 @@ unnecessary_list_index_lookup.py:17:15: PLR1736 [*] Unnecessary lookup of list i
Safe fix
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 |- print(letters[index]) # PLR1736
17 |+ print(letter) # PLR1736
@@ -154,7 +154,7 @@ unnecessary_list_index_lookup.py:18:16: PLR1736 [*] Unnecessary lookup of list i
= help: Use existing variable
Safe fix
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 17 | print(letters[index]) # PLR1736
18 |- blah = letters[index] # PLR1736

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unspecified_encoding.py:8:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
unspecified_encoding.py:8:1: PLW1514 `open` in text mode without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
@@ -9,19 +9,8 @@ unspecified_encoding.py:8:1: PLW1514 [*] `open` in text mode without explicit `e
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
|
= help: Add explicit `encoding` argument
Unsafe fix
5 5 | import codecs
6 6 |
7 7 | # Errors.
8 |-open("test.txt")
8 |+open("test.txt", encoding="locale")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
unspecified_encoding.py:9:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
unspecified_encoding.py:9:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
@@ -30,19 +19,8 @@ unspecified_encoding.py:9:1: PLW1514 [*] `io.TextIOWrapper` without explicit `en
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
6 6 |
7 7 | # Errors.
8 8 | open("test.txt")
9 |-io.TextIOWrapper(io.FileIO("test.txt"))
9 |+io.TextIOWrapper(io.FileIO("test.txt"), encoding="locale")
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
unspecified_encoding.py:10:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
unspecified_encoding.py:10:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
|
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
@@ -51,19 +29,8 @@ unspecified_encoding.py:10:1: PLW1514 [*] `io.TextIOWrapper` without explicit `e
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
7 7 | # Errors.
8 8 | open("test.txt")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 |-hugo.TextIOWrapper(hugo.FileIO("test.txt"))
10 |+hugo.TextIOWrapper(hugo.FileIO("test.txt"), encoding="locale")
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
unspecified_encoding.py:11:1: PLW1514 [*] `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
unspecified_encoding.py:11:1: PLW1514 `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
|
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
@@ -72,19 +39,8 @@ unspecified_encoding.py:11:1: PLW1514 [*] `tempfile.NamedTemporaryFile` in text
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
|
= help: Add explicit `encoding` argument
Unsafe fix
8 8 | open("test.txt")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |-tempfile.NamedTemporaryFile("w")
11 |+tempfile.NamedTemporaryFile("w", encoding="locale")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
unspecified_encoding.py:12:1: PLW1514 [*] `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
unspecified_encoding.py:12:1: PLW1514 `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
|
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
@@ -93,19 +49,8 @@ unspecified_encoding.py:12:1: PLW1514 [*] `tempfile.TemporaryFile` in text mode
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 |-tempfile.TemporaryFile("w")
12 |+tempfile.TemporaryFile("w", encoding="locale")
13 13 | codecs.open("test.txt")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
15 15 |
unspecified_encoding.py:13:1: PLW1514 [*] `codecs.open` in text mode without explicit `encoding` argument
unspecified_encoding.py:13:1: PLW1514 `codecs.open` in text mode without explicit `encoding` argument
|
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
@@ -113,19 +58,8 @@ unspecified_encoding.py:13:1: PLW1514 [*] `codecs.open` in text mode without exp
| ^^^^^^^^^^^ PLW1514
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 |-codecs.open("test.txt")
13 |+codecs.open("test.txt", encoding="locale")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
15 15 |
16 16 | # Non-errors.
unspecified_encoding.py:14:1: PLW1514 [*] `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
unspecified_encoding.py:14:1: PLW1514 `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
|
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
@@ -134,223 +68,5 @@ unspecified_encoding.py:14:1: PLW1514 [*] `tempfile.SpooledTemporaryFile` in tex
15 |
16 | # Non-errors.
|
= help: Add explicit `encoding` argument
Unsafe fix
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
14 |-tempfile.SpooledTemporaryFile(0, "w")
14 |+tempfile.SpooledTemporaryFile(0, "w", encoding="locale")
15 15 |
16 16 | # Non-errors.
17 17 | open("test.txt", encoding="utf-8")
unspecified_encoding.py:46:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
44 | tempfile.SpooledTemporaryFile(0, )
45 |
46 | open("test.txt",)
| ^^^^ PLW1514
47 | open()
48 | open(
|
= help: Add explicit `encoding` argument
Unsafe fix
43 43 | tempfile.SpooledTemporaryFile(0, "wb")
44 44 | tempfile.SpooledTemporaryFile(0, )
45 45 |
46 |-open("test.txt",)
46 |+open("test.txt", encoding="locale",)
47 47 | open()
48 48 | open(
49 49 | "test.txt", # comment
unspecified_encoding.py:47:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
| ^^^^ PLW1514
48 | open(
49 | "test.txt", # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
44 44 | tempfile.SpooledTemporaryFile(0, )
45 45 |
46 46 | open("test.txt",)
47 |-open()
47 |+open(encoding="locale")
48 48 | open(
49 49 | "test.txt", # comment
50 50 | )
unspecified_encoding.py:48:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
48 | open(
| ^^^^ PLW1514
49 | "test.txt", # comment
50 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
46 46 | open("test.txt",)
47 47 | open()
48 48 | open(
49 |- "test.txt", # comment
49 |+ "test.txt", encoding="locale", # comment
50 50 | )
51 51 | open(
52 52 | "test.txt",
unspecified_encoding.py:51:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
49 | "test.txt", # comment
50 | )
51 | open(
| ^^^^ PLW1514
52 | "test.txt",
53 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
49 49 | "test.txt", # comment
50 50 | )
51 51 | open(
52 |- "test.txt",
52 |+ "test.txt", encoding="locale",
53 53 | # comment
54 54 | )
55 55 | open(("test.txt"),)
unspecified_encoding.py:55:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
53 | # comment
54 | )
55 | open(("test.txt"),)
| ^^^^ PLW1514
56 | open(
57 | ("test.txt"), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
52 52 | "test.txt",
53 53 | # comment
54 54 | )
55 |-open(("test.txt"),)
55 |+open(("test.txt"), encoding="locale",)
56 56 | open(
57 57 | ("test.txt"), # comment
58 58 | )
unspecified_encoding.py:56:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
54 | )
55 | open(("test.txt"),)
56 | open(
| ^^^^ PLW1514
57 | ("test.txt"), # comment
58 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
54 54 | )
55 55 | open(("test.txt"),)
56 56 | open(
57 |- ("test.txt"), # comment
57 |+ ("test.txt"), encoding="locale", # comment
58 58 | )
59 59 | open(
60 60 | ("test.txt"),
unspecified_encoding.py:59:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
57 | ("test.txt"), # comment
58 | )
59 | open(
| ^^^^ PLW1514
60 | ("test.txt"),
61 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
57 57 | ("test.txt"), # comment
58 58 | )
59 59 | open(
60 |- ("test.txt"),
60 |+ ("test.txt"), encoding="locale",
61 61 | # comment
62 62 | )
63 63 |
unspecified_encoding.py:64:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
62 | )
63 |
64 | open((("test.txt")),)
| ^^^^ PLW1514
65 | open(
66 | (("test.txt")), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
61 61 | # comment
62 62 | )
63 63 |
64 |-open((("test.txt")),)
64 |+open((("test.txt")), encoding="locale",)
65 65 | open(
66 66 | (("test.txt")), # comment
67 67 | )
unspecified_encoding.py:65:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
64 | open((("test.txt")),)
65 | open(
| ^^^^ PLW1514
66 | (("test.txt")), # comment
67 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
63 63 |
64 64 | open((("test.txt")),)
65 65 | open(
66 |- (("test.txt")), # comment
66 |+ (("test.txt")), encoding="locale", # comment
67 67 | )
68 68 | open(
69 69 | (("test.txt")),
unspecified_encoding.py:68:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
66 | (("test.txt")), # comment
67 | )
68 | open(
| ^^^^ PLW1514
69 | (("test.txt")),
70 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
66 66 | (("test.txt")), # comment
67 67 | )
68 68 | open(
69 |- (("test.txt")),
69 |+ (("test.txt")), encoding="locale",
70 70 | # comment
71 71 | )

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_positional_params.py:3:5: PLR0917 Too many positional arguments: (7/4)
|
1 | # Too many positional arguments (7/4) for max_positional=4
2 | # OK for dummy_variable_rgx ~ "skip_.*"
3 | def f(w, x, y, z, skip_t, skip_u, skip_v):
| ^ PLR0917
4 | pass
|
too_many_positional_params.py:9:5: PLR0917 Too many positional arguments: (7/4)
|
7 | # Too many positional arguments (7/4) for max_args=4
8 | # Too many positional arguments (7/3) for dummy_variable_rgx ~ "skip_.*"
9 | def f(w, x, y, z, t, u, v):
| ^ PLR0917
10 | pass
|

View File

@@ -1,477 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unspecified_encoding.py:8:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
| ^^^^ PLW1514
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 |-open("test.txt")
9 |+open("test.txt", encoding=locale.getpreferredencoding(False))
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
unspecified_encoding.py:9:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^ PLW1514
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 |-io.TextIOWrapper(io.FileIO("test.txt"))
10 |+io.TextIOWrapper(io.FileIO("test.txt"), encoding=locale.getpreferredencoding(False))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
unspecified_encoding.py:10:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^^^ PLW1514
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 |-hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |+hugo.TextIOWrapper(hugo.FileIO("test.txt"), encoding=locale.getpreferredencoding(False))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
unspecified_encoding.py:11:1: PLW1514 [*] `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
|
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |-tempfile.NamedTemporaryFile("w")
12 |+tempfile.NamedTemporaryFile("w", encoding=locale.getpreferredencoding(False))
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
14 15 | tempfile.SpooledTemporaryFile(0, "w")
unspecified_encoding.py:12:1: PLW1514 [*] `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
|
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^ PLW1514
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 |-tempfile.TemporaryFile("w")
13 |+tempfile.TemporaryFile("w", encoding=locale.getpreferredencoding(False))
13 14 | codecs.open("test.txt")
14 15 | tempfile.SpooledTemporaryFile(0, "w")
15 16 |
unspecified_encoding.py:13:1: PLW1514 [*] `codecs.open` in text mode without explicit `encoding` argument
|
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
| ^^^^^^^^^^^ PLW1514
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 |-codecs.open("test.txt")
14 |+codecs.open("test.txt", encoding=locale.getpreferredencoding(False))
14 15 | tempfile.SpooledTemporaryFile(0, "w")
15 16 |
16 17 | # Non-errors.
unspecified_encoding.py:14:1: PLW1514 [*] `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
|
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
15 |
16 | # Non-errors.
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
14 |-tempfile.SpooledTemporaryFile(0, "w")
15 |+tempfile.SpooledTemporaryFile(0, "w", encoding=locale.getpreferredencoding(False))
15 16 |
16 17 | # Non-errors.
17 18 | open("test.txt", encoding="utf-8")
unspecified_encoding.py:46:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
44 | tempfile.SpooledTemporaryFile(0, )
45 |
46 | open("test.txt",)
| ^^^^ PLW1514
47 | open()
48 | open(
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
43 44 | tempfile.SpooledTemporaryFile(0, "wb")
44 45 | tempfile.SpooledTemporaryFile(0, )
45 46 |
46 |-open("test.txt",)
47 |+open("test.txt", encoding=locale.getpreferredencoding(False),)
47 48 | open()
48 49 | open(
49 50 | "test.txt", # comment
unspecified_encoding.py:47:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
| ^^^^ PLW1514
48 | open(
49 | "test.txt", # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
44 45 | tempfile.SpooledTemporaryFile(0, )
45 46 |
46 47 | open("test.txt",)
47 |-open()
48 |+open(encoding=locale.getpreferredencoding(False))
48 49 | open(
49 50 | "test.txt", # comment
50 51 | )
unspecified_encoding.py:48:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
48 | open(
| ^^^^ PLW1514
49 | "test.txt", # comment
50 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
46 47 | open("test.txt",)
47 48 | open()
48 49 | open(
49 |- "test.txt", # comment
50 |+ "test.txt", encoding=locale.getpreferredencoding(False), # comment
50 51 | )
51 52 | open(
52 53 | "test.txt",
unspecified_encoding.py:51:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
49 | "test.txt", # comment
50 | )
51 | open(
| ^^^^ PLW1514
52 | "test.txt",
53 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
49 50 | "test.txt", # comment
50 51 | )
51 52 | open(
52 |- "test.txt",
53 |+ "test.txt", encoding=locale.getpreferredencoding(False),
53 54 | # comment
54 55 | )
55 56 | open(("test.txt"),)
unspecified_encoding.py:55:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
53 | # comment
54 | )
55 | open(("test.txt"),)
| ^^^^ PLW1514
56 | open(
57 | ("test.txt"), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
52 53 | "test.txt",
53 54 | # comment
54 55 | )
55 |-open(("test.txt"),)
56 |+open(("test.txt"), encoding=locale.getpreferredencoding(False),)
56 57 | open(
57 58 | ("test.txt"), # comment
58 59 | )
unspecified_encoding.py:56:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
54 | )
55 | open(("test.txt"),)
56 | open(
| ^^^^ PLW1514
57 | ("test.txt"), # comment
58 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
54 55 | )
55 56 | open(("test.txt"),)
56 57 | open(
57 |- ("test.txt"), # comment
58 |+ ("test.txt"), encoding=locale.getpreferredencoding(False), # comment
58 59 | )
59 60 | open(
60 61 | ("test.txt"),
unspecified_encoding.py:59:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
57 | ("test.txt"), # comment
58 | )
59 | open(
| ^^^^ PLW1514
60 | ("test.txt"),
61 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
57 58 | ("test.txt"), # comment
58 59 | )
59 60 | open(
60 |- ("test.txt"),
61 |+ ("test.txt"), encoding=locale.getpreferredencoding(False),
61 62 | # comment
62 63 | )
63 64 |
unspecified_encoding.py:64:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
62 | )
63 |
64 | open((("test.txt")),)
| ^^^^ PLW1514
65 | open(
66 | (("test.txt")), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
61 62 | # comment
62 63 | )
63 64 |
64 |-open((("test.txt")),)
65 |+open((("test.txt")), encoding=locale.getpreferredencoding(False),)
65 66 | open(
66 67 | (("test.txt")), # comment
67 68 | )
unspecified_encoding.py:65:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
64 | open((("test.txt")),)
65 | open(
| ^^^^ PLW1514
66 | (("test.txt")), # comment
67 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
63 64 |
64 65 | open((("test.txt")),)
65 66 | open(
66 |- (("test.txt")), # comment
67 |+ (("test.txt")), encoding=locale.getpreferredencoding(False), # comment
67 68 | )
68 69 | open(
69 70 | (("test.txt")),
unspecified_encoding.py:68:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
66 | (("test.txt")), # comment
67 | )
68 | open(
| ^^^^ PLW1514
69 | (("test.txt")),
70 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
66 67 | (("test.txt")), # comment
67 68 | )
68 69 | open(
69 |- (("test.txt")),
70 |+ (("test.txt")), encoding=locale.getpreferredencoding(False),
70 71 | # comment
71 72 | )

View File

@@ -238,65 +238,4 @@ flowchart TD
block0 --> return
```
## Function 8
### Source
```python
def func():
for i in range(5):
pass
else:
return 1
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0["pass\n"]
block1["return 1\n"]
block2["for i in range(5):
pass
else:
return 1\n"]
start --> block2
block2 -- "range(5)" --> block0
block2 -- "else" --> block1
block1 --> return
block0 --> return
```
## Function 9
### Source
```python
def func():
for i in range(5):
pass
else:
return 1
x = 1
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0["x = 1\n"]
block1["pass\n"]
block2["return 1\n"]
block3["for i in range(5):
pass
else:
return 1\n"]
start --> block3
block3 -- "range(5)" --> block1
block3 -- "else" --> block2
block2 --> return
block1 --> block3
block0 --> return
```

View File

@@ -773,43 +773,4 @@ flowchart TD
block0 --> return
```
## Function 14
### Source
```python
def func(point):
match point:
case (0, 0):
print("Origin")
case foo:
raise ValueError("oops")
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0[["`*(empty)*`"]]
block1["raise ValueError(#quot;oops#quot;)\n"]
block2["match point:
case (0, 0):
print(#quot;Origin#quot;)
case foo:
raise ValueError(#quot;oops#quot;)\n"]
block3["print(#quot;Origin#quot;)\n"]
block4["match point:
case (0, 0):
print(#quot;Origin#quot;)
case foo:
raise ValueError(#quot;oops#quot;)\n"]
start --> block4
block4 -- "case (0, 0)" --> block3
block4 -- "else" --> block2
block3 --> block0
block2 --> block1
block1 --> return
block0 --> return
```

View File

@@ -2,8 +2,8 @@ use std::{fmt, iter, usize};
use log::error;
use ruff_python_ast::{
Expr, ExprBooleanLiteral, Identifier, MatchCase, Pattern, PatternMatchAs, PatternMatchOr, Stmt,
StmtFor, StmtMatch, StmtReturn, StmtTry, StmtWhile, StmtWith,
Expr, ExprBooleanLiteral, Identifier, MatchCase, Pattern, PatternMatchAs, Stmt, StmtFor,
StmtMatch, StmtReturn, StmtTry, StmtWhile, StmtWith,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -416,6 +416,13 @@ fn match_case<'stmt>(
}
last_statement_index
};
// TODO: handle named arguments, e.g.
// ```python
// match $subject:
// case $binding:
// print($binding)
// ```
// These should also return `NextBlock::Always`.
let next = if is_wildcard(case) {
// Wildcard case is always taken.
NextBlock::Always(next_block_index)
@@ -429,25 +436,10 @@ fn match_case<'stmt>(
BasicBlock { stmts, next }
}
/// Returns true if the [`MatchCase`] is a wildcard pattern.
/// Returns true if `pattern` is a wildcard (`_`) pattern.
fn is_wildcard(pattern: &MatchCase) -> bool {
/// Returns true if the [`Pattern`] is a wildcard pattern.
fn is_wildcard_pattern(pattern: &Pattern) -> bool {
match pattern {
Pattern::MatchValue(_)
| Pattern::MatchSingleton(_)
| Pattern::MatchSequence(_)
| Pattern::MatchMapping(_)
| Pattern::MatchClass(_)
| Pattern::MatchStar(_) => false,
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => pattern.is_none(),
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
patterns.iter().all(is_wildcard_pattern)
}
}
}
pattern.guard.is_none() && is_wildcard_pattern(&pattern.pattern)
pattern.guard.is_none()
&& matches!(&pattern.pattern, Pattern::MatchAs(PatternMatchAs { pattern, name, .. }) if pattern.is_none() && name.is_none())
}
#[derive(Debug, Default)]
@@ -485,8 +477,6 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Stmt::AugAssign(_)
| Stmt::AnnAssign(_)
| Stmt::Break(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
| Stmt::Pass(_) => self.unconditional_next_block(after),
Stmt::Continue(_) => {
// NOTE: the next branch gets fixed up in `change_next_block`.
@@ -648,7 +638,6 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Starred(_)
| Expr::Name(_)
| Expr::List(_)
| Expr::IpyEscapeCommand(_)
| Expr::Tuple(_)
| Expr::Slice(_) => self.unconditional_next_block(after),
// TODO: handle these expressions.
@@ -662,10 +651,13 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Await(_)
| Expr::Yield(_)
| Expr::YieldFrom(_) => self.unconditional_next_block(after),
Expr::IpyEscapeCommand(_) => todo!(),
}
}
// The tough branches are done, here is an easy one.
Stmt::Return(_) => NextBlock::Terminate,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
};
// Include any statements in the block that don't divert the control flow.
@@ -898,8 +890,6 @@ fn needs_next_block(stmts: &[Stmt]) -> bool {
| Stmt::AnnAssign(_)
| Stmt::Expr(_)
| Stmt::Pass(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
// TODO: check below.
| Stmt::Break(_)
| Stmt::Continue(_)
@@ -909,6 +899,8 @@ fn needs_next_block(stmts: &[Stmt]) -> bool {
| Stmt::Match(_)
| Stmt::Try(_)
| Stmt::Assert(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}
@@ -927,8 +919,6 @@ fn is_control_flow_stmt(stmt: &Stmt) -> bool {
| Stmt::AugAssign(_)
| Stmt::AnnAssign(_)
| Stmt::Expr(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
| Stmt::Pass(_) => false,
Stmt::Return(_)
| Stmt::For(_)
@@ -941,6 +931,8 @@ fn is_control_flow_stmt(stmt: &Stmt) -> bool {
| Stmt::Assert(_)
| Stmt::Break(_)
| Stmt::Continue(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}

View File

@@ -344,7 +344,6 @@ RUF100_3.py:23:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
23 |+print(a) # noqa: F821 # comment
24 24 | print(a) # noqa: E501, F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
RUF100_3.py:24:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
@@ -363,8 +362,6 @@ RUF100_3.py:24:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 |-print(a) # noqa: E501, F821 comment
24 |+print(a) # noqa: F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
@@ -372,8 +369,6 @@ RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 | print(a) # noqa: E501, F821 comment
25 | print(a) # noqa: E501, F821 comment
| ^^^^^^^^^^^^^^^^^^ RUF100
26 |
27 | print(a) # comment with unicode µ # noqa: E501
|
= help: Remove unused `noqa` directive
@@ -383,50 +378,5 @@ RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 24 | print(a) # noqa: E501, F821 comment
25 |-print(a) # noqa: E501, F821 comment
25 |+print(a) # noqa: F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
28 28 | print(a) # comment with unicode µ # noqa: E501, F821
RUF100_3.py:27:7: F821 Undefined name `a`
|
25 | print(a) # noqa: E501, F821 comment
26 |
27 | print(a) # comment with unicode µ # noqa: E501
| ^ F821
28 | print(a) # comment with unicode µ # noqa: E501, F821
|
RUF100_3.py:27:39: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
25 | print(a) # noqa: E501, F821 comment
26 |
27 | print(a) # comment with unicode µ # noqa: E501
| ^^^^^^^^^^^^ RUF100
28 | print(a) # comment with unicode µ # noqa: E501, F821
|
= help: Remove unused `noqa` directive
Safe fix
24 24 | print(a) # noqa: E501, F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 |-print(a) # comment with unicode µ # noqa: E501
27 |+print(a) # comment with unicode µ
28 28 | print(a) # comment with unicode µ # noqa: E501, F821
RUF100_3.py:28:39: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
27 | print(a) # comment with unicode µ # noqa: E501
28 | print(a) # comment with unicode µ # noqa: E501, F821
| ^^^^^^^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Safe fix
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
28 |-print(a) # comment with unicode µ # noqa: E501, F821
28 |+print(a) # comment with unicode µ # noqa: F821

View File

@@ -911,180 +911,6 @@ where
}
}
/// Returns `true` if the function has an implicit return.
pub fn implicit_return(function: &ast::StmtFunctionDef) -> bool {
/// Returns `true` if the body may break via a `break` statement.
fn sometimes_breaks(stmts: &[Stmt]) -> bool {
for stmt in stmts {
match stmt {
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
if returns(body) {
return false;
}
if sometimes_breaks(orelse) {
return true;
}
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if returns(body) {
return false;
}
if sometimes_breaks(orelse) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if std::iter::once(body)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.any(|body| sometimes_breaks(body))
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if cases.iter().any(|case| sometimes_breaks(&case.body)) {
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
if sometimes_breaks(body)
|| handlers.iter().any(|handler| {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body,
..
}) = handler;
sometimes_breaks(body)
})
|| sometimes_breaks(orelse)
|| sometimes_breaks(finalbody)
{
return true;
}
}
Stmt::With(ast::StmtWith { body, .. }) => {
if sometimes_breaks(body) {
return true;
}
}
Stmt::Break(_) => return true,
Stmt::Return(_) => return false,
Stmt::Raise(_) => return false,
_ => {}
}
}
false
}
/// Returns `true` if the body may break via a `break` statement.
fn always_breaks(stmts: &[Stmt]) -> bool {
for stmt in stmts {
match stmt {
Stmt::Break(_) => return true,
Stmt::Return(_) => return false,
Stmt::Raise(_) => return false,
_ => {}
}
}
false
}
/// Returns `true` if the body contains a branch that ends without an explicit `return` or
/// `raise` statement.
fn returns(stmts: &[Stmt]) -> bool {
for stmt in stmts.iter().rev() {
match stmt {
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
if always_breaks(body) {
return false;
}
if returns(body) {
return true;
}
if returns(orelse) && !sometimes_breaks(body) {
return true;
}
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if always_breaks(body) {
return false;
}
if returns(body) {
return true;
}
if returns(orelse) && !sometimes_breaks(body) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if elif_else_clauses.iter().any(|clause| clause.test.is_none())
&& std::iter::once(body)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.all(|body| returns(body))
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
// Note: we assume the `match` is exhaustive.
if cases.iter().all(|case| returns(&case.body)) {
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
// If the `finally` block returns, the `try` block must also return.
if returns(finalbody) {
return true;
}
// If the `body` or the `else` block returns, the `try` block must also return.
if (returns(body) || returns(orelse))
&& handlers.iter().all(|handler| {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body,
..
}) = handler;
returns(body)
})
{
return true;
}
}
Stmt::With(ast::StmtWith { body, .. }) => {
if returns(body) {
return true;
}
}
Stmt::Return(_) => return true,
Stmt::Raise(_) => return true,
_ => {}
}
}
false
}
!returns(&function.body)
}
/// A [`StatementVisitor`] that collects all `raise` statements in a function or method.
#[derive(Default)]
pub struct RaiseStatementVisitor<'a> {

View File

@@ -3729,6 +3729,204 @@ impl Ranged for crate::nodes::ParameterWithDefault {
}
}
/// An expression that may be parenthesized.
#[derive(Clone, Debug)]
pub struct ParenthesizedExpr {
/// The range of the expression, including any parentheses.
pub range: TextRange,
/// The underlying expression.
pub expr: Expr,
}
impl ParenthesizedExpr {
/// Returns `true` if the expression is may be parenthesized.
pub fn is_parenthesized(&self) -> bool {
self.range != self.expr.range()
}
}
impl Ranged for ParenthesizedExpr {
fn range(&self) -> TextRange {
self.range
}
}
impl From<Expr> for ParenthesizedExpr {
fn from(expr: Expr) -> Self {
ParenthesizedExpr {
range: expr.range(),
expr,
}
}
}
impl From<ParenthesizedExpr> for Expr {
fn from(parenthesized_expr: ParenthesizedExpr) -> Self {
parenthesized_expr.expr
}
}
impl From<ExprIpyEscapeCommand> for ParenthesizedExpr {
fn from(payload: ExprIpyEscapeCommand) -> Self {
Expr::IpyEscapeCommand(payload).into()
}
}
impl From<ExprBoolOp> for ParenthesizedExpr {
fn from(payload: ExprBoolOp) -> Self {
Expr::BoolOp(payload).into()
}
}
impl From<ExprNamedExpr> for ParenthesizedExpr {
fn from(payload: ExprNamedExpr) -> Self {
Expr::NamedExpr(payload).into()
}
}
impl From<ExprBinOp> for ParenthesizedExpr {
fn from(payload: ExprBinOp) -> Self {
Expr::BinOp(payload).into()
}
}
impl From<ExprUnaryOp> for ParenthesizedExpr {
fn from(payload: ExprUnaryOp) -> Self {
Expr::UnaryOp(payload).into()
}
}
impl From<ExprLambda> for ParenthesizedExpr {
fn from(payload: ExprLambda) -> Self {
Expr::Lambda(payload).into()
}
}
impl From<ExprIfExp> for ParenthesizedExpr {
fn from(payload: ExprIfExp) -> Self {
Expr::IfExp(payload).into()
}
}
impl From<ExprDict> for ParenthesizedExpr {
fn from(payload: ExprDict) -> Self {
Expr::Dict(payload).into()
}
}
impl From<ExprSet> for ParenthesizedExpr {
fn from(payload: ExprSet) -> Self {
Expr::Set(payload).into()
}
}
impl From<ExprListComp> for ParenthesizedExpr {
fn from(payload: ExprListComp) -> Self {
Expr::ListComp(payload).into()
}
}
impl From<ExprSetComp> for ParenthesizedExpr {
fn from(payload: ExprSetComp) -> Self {
Expr::SetComp(payload).into()
}
}
impl From<ExprDictComp> for ParenthesizedExpr {
fn from(payload: ExprDictComp) -> Self {
Expr::DictComp(payload).into()
}
}
impl From<ExprGeneratorExp> for ParenthesizedExpr {
fn from(payload: ExprGeneratorExp) -> Self {
Expr::GeneratorExp(payload).into()
}
}
impl From<ExprAwait> for ParenthesizedExpr {
fn from(payload: ExprAwait) -> Self {
Expr::Await(payload).into()
}
}
impl From<ExprYield> for ParenthesizedExpr {
fn from(payload: ExprYield) -> Self {
Expr::Yield(payload).into()
}
}
impl From<ExprYieldFrom> for ParenthesizedExpr {
fn from(payload: ExprYieldFrom) -> Self {
Expr::YieldFrom(payload).into()
}
}
impl From<ExprCompare> for ParenthesizedExpr {
fn from(payload: ExprCompare) -> Self {
Expr::Compare(payload).into()
}
}
impl From<ExprCall> for ParenthesizedExpr {
fn from(payload: ExprCall) -> Self {
Expr::Call(payload).into()
}
}
impl From<ExprFormattedValue> for ParenthesizedExpr {
fn from(payload: ExprFormattedValue) -> Self {
Expr::FormattedValue(payload).into()
}
}
impl From<ExprFString> for ParenthesizedExpr {
fn from(payload: ExprFString) -> Self {
Expr::FString(payload).into()
}
}
impl From<ExprStringLiteral> for ParenthesizedExpr {
fn from(payload: ExprStringLiteral) -> Self {
Expr::StringLiteral(payload).into()
}
}
impl From<ExprBytesLiteral> for ParenthesizedExpr {
fn from(payload: ExprBytesLiteral) -> Self {
Expr::BytesLiteral(payload).into()
}
}
impl From<ExprNumberLiteral> for ParenthesizedExpr {
fn from(payload: ExprNumberLiteral) -> Self {
Expr::NumberLiteral(payload).into()
}
}
impl From<ExprBooleanLiteral> for ParenthesizedExpr {
fn from(payload: ExprBooleanLiteral) -> Self {
Expr::BooleanLiteral(payload).into()
}
}
impl From<ExprNoneLiteral> for ParenthesizedExpr {
fn from(payload: ExprNoneLiteral) -> Self {
Expr::NoneLiteral(payload).into()
}
}
impl From<ExprEllipsisLiteral> for ParenthesizedExpr {
fn from(payload: ExprEllipsisLiteral) -> Self {
Expr::EllipsisLiteral(payload).into()
}
}
impl From<ExprAttribute> for ParenthesizedExpr {
fn from(payload: ExprAttribute) -> Self {
Expr::Attribute(payload).into()
}
}
impl From<ExprSubscript> for ParenthesizedExpr {
fn from(payload: ExprSubscript) -> Self {
Expr::Subscript(payload).into()
}
}
impl From<ExprStarred> for ParenthesizedExpr {
fn from(payload: ExprStarred) -> Self {
Expr::Starred(payload).into()
}
}
impl From<ExprName> for ParenthesizedExpr {
fn from(payload: ExprName) -> Self {
Expr::Name(payload).into()
}
}
impl From<ExprList> for ParenthesizedExpr {
fn from(payload: ExprList) -> Self {
Expr::List(payload).into()
}
}
impl From<ExprTuple> for ParenthesizedExpr {
fn from(payload: ExprTuple) -> Self {
Expr::Tuple(payload).into()
}
}
impl From<ExprSlice> for ParenthesizedExpr {
fn from(payload: ExprSlice) -> Self {
Expr::Slice(payload).into()
}
}
#[cfg(target_pointer_width = "64")]
mod size_assertions {
use static_assertions::assert_eq_size;

View File

@@ -1,12 +1,3 @@
###############################################################################
# DOCTEST CODE EXAMPLES
#
# This section shows examples of docstrings that contain code snippets in
# Python's "doctest" format.
#
# See: https://docs.python.org/3/library/doctest.html
###############################################################################
# The simplest doctest to ensure basic formatting works.
def doctest_simple():
"""

View File

@@ -68,10 +68,6 @@ with True:
with True:
... # comment
with True:
...
# comment
match x:
case 1:
...
@@ -103,4 +99,4 @@ try:
except:
... # comment
finally:
... # comment
... # comment

View File

@@ -16,12 +16,10 @@ type Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
type Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx[Aaaaaaaaaaaaaaaaaaaaaaaaaaaa] = int
type Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx[Aaaaaaaaaaaaaaaaaaaaaaaaaaaa, Bbbbbbbbbbbbb] = int
type Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = Tttttttttttttttttttttttttttttttttttttttttttttttttttttttt
type Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = Tttttttttttttttttttttttttttttttttttttttttttttttttttttttt # with comment
# long value
type X = Ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt
type X = Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | Ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
type XXXXXXXXXXXXX = Tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt # with comment
# soft keyword as alias name
type type = int

View File

@@ -74,11 +74,6 @@ impl<'a> PyFormatContext<'a> {
..self
}
}
/// Returns `true` if preview mode is enabled.
pub(crate) const fn is_preview(&self) -> bool {
self.options.preview().is_enabled()
}
}
impl FormatContext for PyFormatContext<'_> {

View File

@@ -13,15 +13,14 @@ use ruff_text_size::{Ranged, TextRange};
use crate::comments::{leading_comments, trailing_comments, Comments, SourceComment};
use crate::expression::parentheses::{
in_parentheses_only_group, in_parentheses_only_if_group_breaks,
in_parentheses_only_soft_line_break, in_parentheses_only_soft_line_break_or_space,
is_expression_parenthesized, write_in_parentheses_only_group_end_tag,
write_in_parentheses_only_group_start_tag, Parentheses,
in_parentheses_only_group, in_parentheses_only_soft_line_break,
in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized,
write_in_parentheses_only_group_end_tag, write_in_parentheses_only_group_start_tag,
Parentheses,
};
use crate::expression::string::{AnyString, FormatString, StringLayout};
use crate::expression::OperatorPrecedence;
use crate::prelude::*;
use crate::preview::is_fix_power_op_line_length_enabled;
#[derive(Copy, Clone, Debug)]
pub(super) enum BinaryLike<'a> {
@@ -719,11 +718,7 @@ impl Format<PyFormatContext<'_>> for FlatBinaryExpressionSlice<'_> {
)
{
hard_line_break().fmt(f)?;
} else if is_pow {
if is_fix_power_op_line_length_enabled(f.context()) {
in_parentheses_only_if_group_breaks(&space()).fmt(f)?;
}
} else {
} else if !is_pow {
space().fmt(f)?;
}

View File

@@ -12,7 +12,9 @@ use ruff_python_trivia::CommentRanges;
use ruff_text_size::Ranged;
use crate::builders::parenthesize_if_expands;
use crate::comments::{leading_comments, trailing_comments, LeadingDanglingTrailingComments};
use crate::comments::{
leading_comments, trailing_comments, LeadingDanglingTrailingComments, SourceComment,
};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::expr_generator_exp::is_generator_parenthesized;
use crate::expression::expr_tuple::is_tuple_parenthesized;
@@ -21,7 +23,7 @@ use crate::expression::parentheses::{
OptionalParentheses, Parentheses, Parenthesize,
};
use crate::prelude::*;
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
use crate::PyFormatOptions;
mod binary_like;
pub(crate) mod expr_attribute;
@@ -129,7 +131,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
let node_comments = comments.leading_dangling_trailing(expression);
if !node_comments.has_leading() && !node_comments.has_trailing() {
parenthesized("(", &format_expr, ")")
.with_indent(!is_expression_huggable(expression, f.context()))
.with_indent(!is_expression_huggable(expression, f.options()))
.fmt(f)
} else {
format_with_parentheses_comments(expression, &node_comments, f)
@@ -432,23 +434,113 @@ impl Format<PyFormatContext<'_>> for MaybeParenthesizeExpression<'_> {
}
Parenthesize::IfBreaks => {
if node_comments.has_trailing() {
expression.format().with_options(Parentheses::Always).fmt(f)
// Is the expression the last token in the parent statement.
// Excludes `await` and `yield` for which Black doesn't seem to apply the layout?
let last_expression = parent.is_stmt_assign()
|| parent.is_stmt_ann_assign()
|| parent.is_stmt_aug_assign()
|| parent.is_stmt_return();
// Format the statements and value's trailing end of line comments:
// * after the expression if the expression needs no parentheses (necessary or the `expand_parent` makes the group never fit).
// * inside the parentheses if the expression exceeds the line-width.
//
// ```python
// a = long # with_comment
// b = (
// short # with_comment
// )
//
// # formatted
// a = (
// long # with comment
// )
// b = short # with comment
// ```
// This matches Black's formatting with the exception that ruff applies this style also for
// attribute chains and non-fluent call expressions. See https://github.com/psf/black/issues/4001#issuecomment-1786681792
//
// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because
// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement.
let (inline_comments, expression_trailing_comments) = if last_expression
&& !(
// Ignore non-fluent attribute chains for black compatibility.
// See https://github.com/psf/black/issues/4001#issuecomment-1786681792
expression.is_attribute_expr()
|| expression.is_call_expr()
|| expression.is_yield_from_expr()
|| expression.is_yield_expr()
|| expression.is_await_expr()
) {
let parent_trailing_comments = comments.trailing(*parent);
let after_end_of_line = parent_trailing_comments
.partition_point(|comment| comment.line_position().is_end_of_line());
let (stmt_inline_comments, _) =
parent_trailing_comments.split_at(after_end_of_line);
let after_end_of_line = node_comments
.trailing
.partition_point(|comment| comment.line_position().is_end_of_line());
let (expression_inline_comments, expression_trailing_comments) =
node_comments.trailing.split_at(after_end_of_line);
(
OptionalParenthesesInlinedComments {
expression: expression_inline_comments,
statement: stmt_inline_comments,
},
expression_trailing_comments,
)
} else {
(
OptionalParenthesesInlinedComments::default(),
node_comments.trailing,
)
};
if expression_trailing_comments.is_empty() {
// The group id is necessary because the nested expressions may reference it.
let group_id = f.group_id("optional_parentheses");
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
best_fit_parenthesize(&expression.format().with_options(Parentheses::Never))
.with_group_id(Some(group_id))
.fmt(f)
best_fit_parenthesize(&format_with(|f| {
inline_comments.mark_formatted();
expression
.format()
.with_options(Parentheses::Never)
.fmt(f)?;
if !inline_comments.is_empty() {
// If the expressions exceeds the line width, format the comments in the parentheses
if_group_breaks(&inline_comments)
.with_group_id(Some(group_id))
.fmt(f)?;
}
Ok(())
}))
.with_group_id(Some(group_id))
.fmt(f)?;
if !inline_comments.is_empty() {
// If the line fits into the line width, format the comments after the parenthesized expression
if_group_fits_on_line(&inline_comments)
.with_group_id(Some(group_id))
.fmt(f)?;
}
Ok(())
} else {
expression.format().with_options(Parentheses::Always).fmt(f)
}
}
},
OptionalParentheses::Never => match parenthesize {
Parenthesize::IfBreaksOrIfRequired => {
parenthesize_if_expands(&expression.format().with_options(Parentheses::Never))
.with_indent(!is_expression_huggable(expression, f.context()))
.with_indent(!is_expression_huggable(expression, f.options()))
.fmt(f)
}
@@ -1061,8 +1153,8 @@ pub(crate) fn has_own_parentheses(
/// ]
/// )
/// ```
pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> bool {
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
pub(crate) fn is_expression_huggable(expr: &Expr, options: &PyFormatOptions) -> bool {
if !options.preview().is_enabled() {
return false;
}
@@ -1156,3 +1248,41 @@ impl From<Operator> for OperatorPrecedence {
}
}
}
#[derive(Debug, Default)]
struct OptionalParenthesesInlinedComments<'a> {
expression: &'a [SourceComment],
statement: &'a [SourceComment],
}
impl<'a> OptionalParenthesesInlinedComments<'a> {
fn is_empty(&self) -> bool {
self.expression.is_empty() && self.statement.is_empty()
}
fn iter_comments(&self) -> impl Iterator<Item = &'a SourceComment> {
self.expression.iter().chain(self.statement)
}
fn mark_formatted(&self) {
for comment in self.iter_comments() {
comment.mark_formatted();
}
}
}
impl Format<PyFormatContext<'_>> for OptionalParenthesesInlinedComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
for comment in self.iter_comments() {
comment.mark_unformatted();
}
write!(
f,
[
trailing_comments(self.expression),
trailing_comments(self.statement)
]
)
}
}

View File

@@ -355,26 +355,6 @@ pub(super) fn write_in_parentheses_only_group_end_tag(f: &mut PyFormatter) {
}
}
/// Shows prints `content` only if the expression is enclosed by (optional) parentheses (`()`, `[]`, or `{}`)
/// and splits across multiple lines.
pub(super) fn in_parentheses_only_if_group_breaks<'a, T>(
content: T,
) -> impl Format<PyFormatContext<'a>>
where
T: Format<PyFormatContext<'a>>,
{
format_with(move |f: &mut PyFormatter| match f.context().node_level() {
NodeLevel::TopLevel(_) | NodeLevel::CompoundStatement | NodeLevel::Expression(None) => {
// no-op, not parenthesized
Ok(())
}
NodeLevel::Expression(Some(parentheses_id)) => if_group_breaks(&content)
.with_group_id(Some(parentheses_id))
.fmt(f),
NodeLevel::ParenthesizedExpression => if_group_breaks(&content).fmt(f),
})
}
/// Format comments inside empty parentheses, brackets or curly braces.
///
/// Empty `()`, `[]` and `{}` are special because there can be dangling comments, and they can be in

View File

@@ -244,10 +244,10 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
mut lines: std::iter::Peekable<std::str::Lines<'src>>,
) -> FormatResult<()> {
while let Some(line) = lines.next() {
let line = InputDocstringLine {
line,
let line = DocstringLine {
line: Cow::Borrowed(line),
offset: self.offset,
next: lines.peek().copied(),
is_last: lines.peek().is_none(),
};
// We know that the normalized string has \n line endings.
self.offset += line.line.text_len() + "\n".text_len();
@@ -262,7 +262,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// immediately to the underlying buffer. If the line starts or is part
/// of an existing code snippet, then the lines will get buffered until
/// the code snippet is complete.
fn add_one(&mut self, line: InputDocstringLine<'src>) -> FormatResult<()> {
fn add_one(&mut self, line: DocstringLine<'src>) -> FormatResult<()> {
// Just pass through the line as-is without looking for a code snippet
// when docstring code formatting is disabled. And also when we are
// formatting a code snippet so as to avoid arbitrarily nested code
@@ -271,49 +271,45 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
// not clear that it's worth the effort to support.
if !self.f.options().docstring_code().is_enabled() || self.f.context().docstring().is_some()
{
return self.print_one(&line.as_output());
return self.print_one(&line);
}
match self.code_example.add(line) {
CodeExampleAddAction::Print { original } => self.print_one(&original.as_output())?,
CodeExampleAddAction::Print { original } => self.print_one(&original)?,
CodeExampleAddAction::Kept => {}
CodeExampleAddAction::Reset { code, original } => {
for codeline in code {
self.print_one(&codeline.original.as_output())?;
}
self.print_one(&original.as_output())?;
}
CodeExampleAddAction::Format { mut kind, original } => {
let Some(formatted_lines) = self.format(kind.code())? else {
CodeExampleAddAction::Format {
kind,
code,
original,
} => {
let Some(formatted_lines) = self.format(&code)? else {
// If formatting failed in a way that should not be
// allowed, we back out what we're doing and print the
// original lines we found as-is as if we did nothing.
for codeline in kind.code() {
self.print_one(&codeline.original.as_output())?;
for codeline in code {
self.print_one(&codeline.original)?;
}
if let Some(original) = original {
self.print_one(&original.as_output())?;
self.print_one(&original)?;
}
return Ok(());
};
self.already_normalized = false;
match kind {
CodeExampleKind::Doctest(CodeExampleDoctest { ps1_indent, .. }) => {
CodeExampleKind::Doctest(CodeExampleDoctest { indent }) => {
let mut lines = formatted_lines.into_iter();
if let Some(first) = lines.next() {
self.print_one(
&first.map(|line| std::format!("{ps1_indent}>>> {line}")),
)?;
self.print_one(&first.map(|line| std::format!("{indent}>>> {line}")))?;
for docline in lines {
self.print_one(
&docline.map(|line| std::format!("{ps1_indent}... {line}")),
&docline.map(|line| std::format!("{indent}... {line}")),
)?;
}
}
}
}
if let Some(original) = original {
self.print_one(&original.as_output())?;
self.print_one(&original)?;
}
}
}
@@ -325,7 +321,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// This mostly just handles indentation and ensuring line breaks are
/// inserted as appropriate before passing it on to the formatter to
/// print to the buffer.
fn print_one(&mut self, line: &OutputDocstringLine<'_>) -> FormatResult<()> {
fn print_one(&mut self, line: &DocstringLine<'_>) -> FormatResult<()> {
let trim_end = line.line.trim_end();
if trim_end.is_empty() {
return if line.is_last {
@@ -395,14 +391,10 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// routine is silent about it. So from the user's perspective, this will
/// fail silently. Ideally, this would at least emit a warning message,
/// but at time of writing, it wasn't clear to me how to best do that.
///
/// # Panics
///
/// This panics when the given slice is empty.
fn format(
&mut self,
code: &[CodeExampleLine<'_>],
) -> FormatResult<Option<Vec<OutputDocstringLine<'static>>>> {
code: &[CodeExampleLine<'src>],
) -> FormatResult<Option<Vec<DocstringLine<'static>>>> {
use ruff_python_parser::AsMode;
let offset = code
@@ -414,10 +406,10 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
.last()
.expect("code blob must be non-empty")
.original
.is_last();
.is_last;
let codeblob = code
.iter()
.map(|line| line.code)
.map(|line| &*line.code)
.collect::<Vec<&str>>()
.join("\n");
let printed = match docstring_format_source(self.f.options(), self.quote_style, &codeblob) {
@@ -459,8 +451,8 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
let mut lines = printed
.as_code()
.lines()
.map(|line| OutputDocstringLine {
line: Cow::Owned(line.to_string()),
.map(|line| DocstringLine {
line: Cow::Owned(line.into()),
offset,
is_last: false,
})
@@ -474,72 +466,29 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// Represents a single line in a docstring.
///
/// This type is only used to represent the original lines in a docstring.
/// Specifically, the line contained in this type has no changes from the input
/// source.
#[derive(Clone, Copy, Debug)]
struct InputDocstringLine<'src> {
/// This type is used to both represent the original lines in a docstring
/// (the line will be borrowed) and also the newly formatted lines from code
/// snippets (the line will be owned).
#[derive(Clone, Debug)]
struct DocstringLine<'src> {
/// The actual text of the line, not including the line terminator.
///
/// In practice, this line is borrowed when it corresponds to an original
/// unformatted line in a docstring, and owned when it corresponds to a
/// reformatted line (e.g., from a code snippet) in a docstring.
line: &'src str,
/// The offset into the source document which this line corresponds to.
offset: TextSize,
/// For any input line that isn't the last line, this contains a reference
/// to the line immediately following this one.
///
/// This is `None` if and only if this is the last line in the docstring.
next: Option<&'src str>,
}
impl<'src> InputDocstringLine<'src> {
/// Borrow this input docstring line as an output docstring line.
fn as_output(&self) -> OutputDocstringLine<'src> {
OutputDocstringLine {
line: Cow::Borrowed(self.line),
offset: self.offset,
is_last: self.is_last(),
}
}
/// Whether this is the last line in the docstring or not.
fn is_last(&self) -> bool {
self.next.is_none()
}
}
/// Represents a single reformatted code line in a docstring.
///
/// An input source line may be cheaply converted to an output source line.
/// This is the common case: an input source line is printed pretty much as it
/// is, with perhaps some whitespace normalization applied. The less common
/// case is that the output docstring line owns its `line` because it was
/// produced by reformatting a code snippet.
#[derive(Clone, Debug)]
struct OutputDocstringLine<'src> {
/// The output line.
///
/// This is an owned variant in precisely the cases where it corresponds to
/// a line from a reformatted code snippet. In other cases, it is borrowed
/// from the input docstring line as-is.
line: Cow<'src, str>,
/// The offset into the source document which this line corresponds to.
/// Currently, this is an estimate.
offset: TextSize,
/// Whether this is the last line in a docstring or not. This is determined
/// by whether the last line in the code snippet was also the last line in
/// the docstring. If it was, then it follows that the last line in the
/// reformatted code snippet is also the last line in the docstring.
/// Whether this is the last line in a docstring or not. "Last" lines have
/// some special treatment when printing.
is_last: bool,
}
impl<'src> OutputDocstringLine<'src> {
/// Return this reformatted line, but with the given function applied to
/// the text of the line.
fn map(self, mut map: impl FnMut(&str) -> String) -> OutputDocstringLine<'static> {
OutputDocstringLine {
impl<'src> DocstringLine<'src> {
/// Return this line, but with the given function applied to the text of
/// the line.
fn map(self, mut map: impl FnMut(&str) -> String) -> DocstringLine<'static> {
DocstringLine {
line: Cow::Owned(map(&self.line)),
..self
}
@@ -558,10 +507,9 @@ impl<'src> OutputDocstringLine<'src> {
struct CodeExample<'src> {
/// The kind of code example being collected, or `None` if no code example
/// has been observed.
///
/// The kind is split out into a separate type so that we can pass it
/// around and have a guarantee that a code example actually exists.
kind: Option<CodeExampleKind<'src>>,
kind: Option<CodeExampleKind>,
/// The lines that have been seen so far that make up the code example.
lines: Vec<CodeExampleLine<'src>>,
}
impl<'src> CodeExample<'src> {
@@ -572,7 +520,7 @@ impl<'src> CodeExample<'src> {
/// the caller to perform. The typical case is a "print" action, which
/// instructs the caller to just print the line as though it were not part
/// of a code snippet.
fn add(&mut self, original: InputDocstringLine<'src>) -> CodeExampleAddAction<'src> {
fn add(&mut self, original: DocstringLine<'src>) -> CodeExampleAddAction<'src> {
match self.kind.take() {
// There's no existing code example being built, so we look for
// the start of one or otherwise tell the caller we couldn't find
@@ -581,15 +529,19 @@ impl<'src> CodeExample<'src> {
None => CodeExampleAddAction::Kept,
Some(original) => CodeExampleAddAction::Print { original },
},
Some(CodeExampleKind::Doctest(mut doctest)) => {
if doctest.add_code_line(original) {
Some(CodeExampleKind::Doctest(doctest)) => {
if let Some(code) = doctest_find_ps2_prompt(&doctest.indent, &original.line) {
let code = code.to_string();
self.lines.push(CodeExampleLine { original, code });
// Stay with the doctest kind while we accumulate all
// PS2 prompts.
self.kind = Some(CodeExampleKind::Doctest(doctest));
return CodeExampleAddAction::Kept;
}
let code = std::mem::take(&mut self.lines);
let original = self.add_start(original);
CodeExampleAddAction::Format {
code,
kind: CodeExampleKind::Doctest(doctest),
original,
}
@@ -606,13 +558,13 @@ impl<'src> CodeExample<'src> {
/// This panics when the existing code-example is any non-None value. That
/// is, this routine assumes that there is no ongoing code example being
/// collected and looks for the beginning of another code example.
fn add_start(
&mut self,
original: InputDocstringLine<'src>,
) -> Option<InputDocstringLine<'src>> {
assert!(self.kind.is_none(), "expected no existing code example");
if let Some(doctest) = CodeExampleDoctest::new(original) {
self.kind = Some(CodeExampleKind::Doctest(doctest));
fn add_start(&mut self, original: DocstringLine<'src>) -> Option<DocstringLine<'src>> {
assert_eq!(None, self.kind, "expected no existing code example");
if let Some((indent, code)) = doctest_find_ps1_prompt(&original.line) {
let indent = indent.to_string();
let code = code.to_string();
self.lines.push(CodeExampleLine { original, code });
self.kind = Some(CodeExampleKind::Doctest(CodeExampleDoctest { indent }));
return None;
}
Some(original)
@@ -620,8 +572,8 @@ impl<'src> CodeExample<'src> {
}
/// The kind of code example observed in a docstring.
#[derive(Debug)]
enum CodeExampleKind<'src> {
#[derive(Clone, Debug, Eq, PartialEq)]
enum CodeExampleKind {
/// Code found in Python "doctests."
///
/// Documentation describing doctests and how they're recognized can be
@@ -632,90 +584,17 @@ enum CodeExampleKind<'src> {
/// doctest module to determine more precisely how it works.)
///
/// [regex matching]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L611-L622
Doctest(CodeExampleDoctest<'src>),
}
impl<'src> CodeExampleKind<'src> {
/// Return the lines of code collected so far for this example.
///
/// This is borrowed mutably because it may need to mutate the code lines
/// based on the state accrued so far.
fn code(&mut self) -> &[CodeExampleLine<'src>] {
match *self {
CodeExampleKind::Doctest(ref doctest) => &doctest.lines,
}
}
Doctest(CodeExampleDoctest),
}
/// State corresponding to a single doctest code example found in a docstring.
#[derive(Debug)]
struct CodeExampleDoctest<'src> {
/// The lines that have been seen so far that make up the doctest.
lines: Vec<CodeExampleLine<'src>>,
#[derive(Clone, Debug, Eq, PartialEq)]
struct CodeExampleDoctest {
/// The indent observed in the first doctest line.
///
/// More precisely, this corresponds to the whitespace observed before
/// the starting `>>> ` (the "PS1 prompt").
ps1_indent: &'src str,
}
impl<'src> CodeExampleDoctest<'src> {
/// Looks for a valid doctest PS1 prompt in the line given.
///
/// If one was found, then state for a new doctest code example is
/// returned, along with the code example line.
fn new(original: InputDocstringLine<'src>) -> Option<CodeExampleDoctest<'src>> {
let trim_start = original.line.trim_start();
// Prompts must be followed by an ASCII space character[1].
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812
let code = trim_start.strip_prefix(">>> ")?;
let indent_len = original
.line
.len()
.checked_sub(trim_start.len())
.expect("suffix is <= original");
let lines = vec![CodeExampleLine { original, code }];
let ps1_indent = &original.line[..indent_len];
let doctest = CodeExampleDoctest { lines, ps1_indent };
Some(doctest)
}
/// Looks for a valid doctest PS2 prompt in the line given.
///
/// If one is found, then the code portion of the line following the PS2 prompt
/// is returned.
///
/// Callers must provide a string containing the original indentation of the
/// PS1 prompt that started the doctest containing the potential PS2 prompt
/// in the line given. If the line contains a PS2 prompt, its indentation must
/// match the indentation used for the corresponding PS1 prompt (otherwise
/// `None` will be returned).
fn add_code_line(&mut self, original: InputDocstringLine<'src>) -> bool {
let Some((ps2_indent, ps2_after)) = original.line.split_once("...") else {
return false;
};
// PS2 prompts must have the same indentation as their
// corresponding PS1 prompt.[1] While the 'doctest' Python
// module will error in this case, we just treat this line as a
// non-doctest line.
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L733
if self.ps1_indent != ps2_indent {
return false;
}
// PS2 prompts must be followed by an ASCII space character unless
// it's an otherwise empty line[1].
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812
let code = match ps2_after.strip_prefix(' ') {
None if ps2_after.is_empty() => "",
None => return false,
Some(code) => code,
};
self.lines.push(CodeExampleLine { original, code });
true
}
indent: String,
}
/// A single line in a code example found in a docstring.
@@ -736,9 +615,9 @@ struct CodeExampleLine<'src> {
/// The normalized (but original) line from the doc string. This might, for
/// example, contain a `>>> ` or `... ` prefix if this code example is a
/// doctest.
original: InputDocstringLine<'src>,
original: DocstringLine<'src>,
/// The code extracted from the line.
code: &'src str,
code: String,
}
/// An action that a caller should perform after attempting to add a line from
@@ -754,7 +633,7 @@ enum CodeExampleAddAction<'src> {
///
/// This is the common case. That is, most lines in most docstrings are not
/// part of a code example.
Print { original: InputDocstringLine<'src> },
Print { original: DocstringLine<'src> },
/// The line added was kept by `CodeExample` as part of a new or existing
/// code example.
///
@@ -768,31 +647,70 @@ enum CodeExampleAddAction<'src> {
/// callers should pass it through to the formatter as-is.
Format {
/// The kind of code example that was found.
kind: CodeExampleKind,
/// The Python code that should be formatted, indented and printed.
///
/// This is guaranteed to have a non-empty code snippet.
kind: CodeExampleKind<'src>,
/// This is guaranteed to be non-empty.
code: Vec<CodeExampleLine<'src>>,
/// When set, the line is considered not part of any code example and
/// should be formatted as if the [`Print`] action were returned.
/// Otherwise, if there is no line, then either one does not exist
/// or it is part of another code example and should be treated as a
/// [`Kept`] action.
original: Option<InputDocstringLine<'src>>,
},
/// This occurs when adding a line to an existing code example
/// results in that code example becoming invalid. In this case,
/// we don't want to treat it as a code example, but instead write
/// back the lines to the docstring unchanged.
#[allow(dead_code)] // FIXME: remove when reStructuredText support is added
Reset {
/// The lines of code that we collected but should be printed back to
/// the docstring as-is and not formatted.
code: Vec<CodeExampleLine<'src>>,
/// The line that was added and triggered this reset to occur. It
/// should be written back to the docstring as-is after the code lines.
original: InputDocstringLine<'src>,
original: Option<DocstringLine<'src>>,
},
}
/// Looks for a valid doctest PS1 prompt in the line given.
///
/// If one was found, then the indentation prior to the prompt is returned
/// along with the code portion of the line.
fn doctest_find_ps1_prompt(line: &str) -> Option<(&str, &str)> {
let trim_start = line.trim_start();
// Prompts must be followed by an ASCII space character[1].
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812
let code = trim_start.strip_prefix(">>> ")?;
let indent_len = line
.len()
.checked_sub(trim_start.len())
.expect("suffix is <= original");
let indent = &line[..indent_len];
Some((indent, code))
}
/// Looks for a valid doctest PS2 prompt in the line given.
///
/// If one is found, then the code portion of the line following the PS2 prompt
/// is returned.
///
/// Callers must provide a string containing the original indentation of the
/// PS1 prompt that started the doctest containing the potential PS2 prompt
/// in the line given. If the line contains a PS2 prompt, its indentation must
/// match the indentation used for the corresponding PS1 prompt (otherwise
/// `None` will be returned).
fn doctest_find_ps2_prompt<'src>(ps1_indent: &str, line: &'src str) -> Option<&'src str> {
let (ps2_indent, ps2_after) = line.split_once("...")?;
// PS2 prompts must have the same indentation as their
// corresponding PS1 prompt.[1] While the 'doctest' Python
// module will error in this case, we just treat this line as a
// non-doctest line.
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L733
if ps1_indent != ps2_indent {
return None;
}
// PS2 prompts must be followed by an ASCII space character unless
// it's an otherwise empty line[1].
//
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812
match ps2_after.strip_prefix(' ') {
None if ps2_after.is_empty() => Some(""),
None => None,
Some(code) => Some(code),
}
}
/// Formats the given source code using the given options.
///
/// The given quote style should correspond to the style used by the docstring

View File

@@ -32,7 +32,6 @@ mod options;
pub(crate) mod other;
pub(crate) mod pattern;
mod prelude;
mod preview;
mod shared_traits;
pub(crate) mod statement;
pub(crate) mod type_param;

View File

@@ -119,7 +119,7 @@ impl PyFormatOptions {
self.docstring_code
}
pub const fn preview(&self) -> PreviewMode {
pub fn preview(&self) -> PreviewMode {
self.preview
}

View File

@@ -9,7 +9,6 @@ use crate::expression::is_expression_huggable;
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
use crate::other::commas;
use crate::prelude::*;
use crate::preview::is_hug_parens_with_braces_and_square_brackets_enabled;
#[derive(Default)]
pub struct FormatArguments;
@@ -178,7 +177,8 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source:
/// Hugging should only be applied to single-argument collections, like lists, or starred versions
/// of those collections.
fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
if !is_hug_parens_with_braces_and_square_brackets_enabled(context) {
let options = context.options();
if !options.preview().is_enabled() {
return false;
}
@@ -192,7 +192,7 @@ fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
};
// If the expression itself isn't huggable, then we can't hug it.
if !is_expression_huggable(arg, context) {
if !is_expression_huggable(arg, options) {
return false;
}
@@ -202,8 +202,6 @@ fn is_argument_huggable(item: &Arguments, context: &PyFormatContext) -> bool {
return false;
}
let options = context.options();
// If the expression has a trailing comma, then we can't hug it.
if options.magic_trailing_comma().is_respect()
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), item.end()), options, context)

View File

@@ -1,19 +0,0 @@
//! Helpers to test if a specific preview style is enabled or not.
//!
//! The motivation for these functions isn't to avoid code duplication but to ease promoting preview styles
//! to stable. The challenge with directly using [`is_preview`](PyFormatContext::is_preview) is that it is unclear
//! for which specific feature this preview check is for. Having named functions simplifies the promotion:
//! Simply delete the function and let Rust tell you which checks you have to remove.
use crate::PyFormatContext;
/// Returns `true` if the [`fix_power_op_line_length`](https://github.com/astral-sh/ruff/issues/8938) preview style is enabled.
pub(crate) const fn is_fix_power_op_line_length_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// Returns `true` if the [`hug_parens_with_braces_and_square_brackets`](https://github.com/astral-sh/ruff/issues/8279) preview style is enabled.
pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}

View File

@@ -2,8 +2,10 @@ use ruff_formatter::write;
use ruff_python_ast::StmtAnnAssign;
use crate::comments::{SourceComment, SuppressionKind};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
use crate::statement::trailing_semicolon;
#[derive(Default)]
@@ -31,7 +33,7 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
space(),
token("="),
space(),
FormatStatementsLastExpression::new(value, item)
maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks)
]
)?;
}

View File

@@ -1,11 +1,9 @@
use ruff_formatter::{format_args, write, FormatError};
use ruff_python_ast::{AnyNodeRef, Expr, StmtAssign};
use ruff_python_ast::{Expr, StmtAssign};
use crate::comments::{trailing_comments, SourceComment, SuppressionKind};
use crate::comments::{SourceComment, SuppressionKind};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize,
};
use crate::expression::parentheses::{Parentheses, Parenthesize};
use crate::expression::{has_own_parentheses, maybe_parenthesize_expression};
use crate::prelude::*;
use crate::statement::trailing_semicolon;
@@ -36,7 +34,14 @@ impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
]
)?;
FormatStatementsLastExpression::new(value, item).fmt(f)?;
write!(
f,
[maybe_parenthesize_expression(
value,
item,
Parenthesize::IfBreaks
)]
)?;
if f.options().source_type().is_ipynb()
&& f.context().node_level().is_last_top_level_statement()
@@ -128,188 +133,3 @@ enum ParenthesizeTarget {
Never,
IfBreaks,
}
/// Formats the last expression in statements that start with a keyword (like `return`) or after an operator (assignments).
///
/// It avoids parenthesizing unsplittable values (like `None`, `True`, `False`, Names, a subset of strings) just to make
/// the trailing comment fit and inlines a trailing comment if the value itself exceeds the configured line width:
///
/// The implementation formats the statement's and value's trailing end of line comments:
/// * after the expression if the expression needs no parentheses (necessary or the `expand_parent` makes the group never fit).
/// * inside the parentheses if the expression exceeds the line-width.
///
/// ```python
/// a = loooooooooooooooooooooooooooong # with_comment
/// b = (
/// short # with_comment
/// )
/// ```
///
/// Which gets formatted to:
///
/// ```python
/// # formatted
/// a = (
/// loooooooooooooooooooooooooooong # with comment
/// )
/// b = short # with comment
/// ```
///
/// The long name gets parenthesized because it exceeds the configured line width and the trailing comma of the
/// statement gets formatted inside (instead of outside) the parentheses.
///
/// The `short` name gets unparenthesized because it fits into the configured line length, regardless of whether
/// the comment exceeds the line width or not.
///
/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because
/// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement.
pub(super) struct FormatStatementsLastExpression<'a> {
expression: &'a Expr,
parent: AnyNodeRef<'a>,
}
impl<'a> FormatStatementsLastExpression<'a> {
pub(super) fn new<P: Into<AnyNodeRef<'a>>>(expression: &'a Expr, parent: P) -> Self {
Self {
expression,
parent: parent.into(),
}
}
}
impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let can_inline_comment = match self.expression {
Expr::Name(_)
| Expr::NoneLiteral(_)
| Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_) => true,
Expr::StringLiteral(string) => {
string.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit
}
Expr::BytesLiteral(bytes) => {
bytes.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit
}
Expr::FString(fstring) => {
fstring.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit
}
_ => false,
};
if !can_inline_comment {
return maybe_parenthesize_expression(
self.expression,
self.parent,
Parenthesize::IfBreaks,
)
.fmt(f);
}
let comments = f.context().comments().clone();
let expression_comments = comments.leading_dangling_trailing(self.expression);
if expression_comments.has_leading() {
// Preserve the parentheses if the expression has any leading comments,
// same as `maybe_parenthesize_expression`
return self
.expression
.format()
.with_options(Parentheses::Always)
.fmt(f);
}
let statement_trailing_comments = comments.trailing(self.parent);
let after_end_of_line = statement_trailing_comments
.partition_point(|comment| comment.line_position().is_end_of_line());
let (stmt_inline_comments, _) = statement_trailing_comments.split_at(after_end_of_line);
let after_end_of_line = expression_comments
.trailing
.partition_point(|comment| comment.line_position().is_end_of_line());
let (expression_inline_comments, expression_trailing_comments) =
expression_comments.trailing.split_at(after_end_of_line);
if expression_trailing_comments.is_empty() {
let inline_comments = OptionalParenthesesInlinedComments {
expression: expression_inline_comments,
statement: stmt_inline_comments,
};
let group_id = f.group_id("optional_parentheses");
let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f);
best_fit_parenthesize(&format_with(|f| {
inline_comments.mark_formatted();
self.expression
.format()
.with_options(Parentheses::Never)
.fmt(f)?;
if !inline_comments.is_empty() {
// If the expressions exceeds the line width, format the comments in the parentheses
if_group_breaks(&inline_comments)
.with_group_id(Some(group_id))
.fmt(f)?;
}
Ok(())
}))
.with_group_id(Some(group_id))
.fmt(f)?;
if !inline_comments.is_empty() {
// If the line fits into the line width, format the comments after the parenthesized expression
if_group_fits_on_line(&inline_comments)
.with_group_id(Some(group_id))
.fmt(f)?;
}
Ok(())
} else {
self.expression
.format()
.with_options(Parentheses::Always)
.fmt(f)
}
}
}
#[derive(Debug, Default)]
struct OptionalParenthesesInlinedComments<'a> {
expression: &'a [SourceComment],
statement: &'a [SourceComment],
}
impl<'a> OptionalParenthesesInlinedComments<'a> {
fn is_empty(&self) -> bool {
self.expression.is_empty() && self.statement.is_empty()
}
fn iter_comments(&self) -> impl Iterator<Item = &'a SourceComment> {
self.expression.iter().chain(self.statement)
}
fn mark_formatted(&self) {
for comment in self.expression {
comment.mark_formatted();
}
}
}
impl Format<PyFormatContext<'_>> for OptionalParenthesesInlinedComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
for comment in self.iter_comments() {
comment.mark_unformatted();
}
write!(
f,
[
trailing_comments(self.expression),
trailing_comments(self.statement)
]
)
}
}

View File

@@ -2,8 +2,10 @@ use ruff_formatter::write;
use ruff_python_ast::StmtAugAssign;
use crate::comments::{SourceComment, SuppressionKind};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
use crate::statement::trailing_semicolon;
use crate::{AsFormat, FormatNodeRule};
@@ -26,7 +28,7 @@ impl FormatNodeRule<StmtAugAssign> for FormatStmtAugAssign {
op.format(),
token("="),
space(),
FormatStatementsLastExpression::new(value, item)
maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks)
]
)?;

View File

@@ -3,8 +3,9 @@ use ruff_python_ast::{Expr, StmtReturn};
use crate::comments::{SourceComment, SuppressionKind};
use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
#[derive(Default)]
pub struct FormatStmtReturn;
@@ -30,7 +31,10 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
Some(value) => {
write!(
f,
[space(), FormatStatementsLastExpression::new(value, item)]
[
space(),
maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks)
]
)
}
None => Ok(()),

View File

@@ -2,8 +2,9 @@ use ruff_formatter::write;
use ruff_python_ast::StmtTypeAlias;
use crate::comments::{SourceComment, SuppressionKind};
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::stmt_assign::FormatStatementsLastExpression;
#[derive(Default)]
pub struct FormatStmtTypeAlias;
@@ -29,7 +30,7 @@ impl FormatNodeRule<StmtTypeAlias> for FormatStmtTypeAlias {
space(),
token("="),
space(),
FormatStatementsLastExpression::new(value, item)
maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks)
]
)
}

View File

@@ -511,9 +511,7 @@ pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> b
let [node] = body else {
return false;
};
value.is_ellipsis_literal_expr()
&& !comments.has_leading(node)
&& !comments.has_trailing_own_line(node)
value.is_ellipsis_literal_expr() && !comments.has_leading(node)
}
_ => false,
}

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