Compare commits
44 Commits
v0.2.0
...
charlie/to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0afde1a3f | ||
|
|
df7fb95cbc | ||
|
|
83195a6030 | ||
|
|
d31d09d7cd | ||
|
|
0f436b71f3 | ||
|
|
cd5bcd815d | ||
|
|
0ccca4083a | ||
|
|
041ce1e166 | ||
|
|
36b752876e | ||
|
|
b3dc565473 | ||
|
|
e708c08b64 | ||
|
|
73902323d5 | ||
|
|
9781563ef6 | ||
|
|
84aea7f0c8 | ||
|
|
b47f85eb69 | ||
|
|
80fc02e7d5 | ||
|
|
55d0e1148c | ||
|
|
1de945e3eb | ||
|
|
e277ba20da | ||
|
|
2e836a4cbe | ||
|
|
57d6cdb8d3 | ||
|
|
602f8b8250 | ||
|
|
a6bc4b2e48 | ||
|
|
c5fa0ccffb | ||
|
|
dd77d29d0e | ||
|
|
ad0121660e | ||
|
|
5c99967c4d | ||
|
|
c53aae0b6f | ||
|
|
2352de2277 | ||
|
|
e0a6034cbb | ||
|
|
25d93053da | ||
|
|
ee5b07d4ca | ||
|
|
e50603caf6 | ||
|
|
c3ca34543f | ||
|
|
4f7fb566f0 | ||
|
|
50bfbcf568 | ||
|
|
ea1c089652 | ||
|
|
b947dde8ad | ||
|
|
d259cd0d32 | ||
|
|
af4db39205 | ||
|
|
467c091382 | ||
|
|
92d99a72d9 | ||
|
|
66d2c1e1c4 | ||
|
|
ded8c7629f |
9
.github/workflows/release.yaml
vendored
9
.github/workflows/release.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
path: dist
|
||||
|
||||
macos-x86_64:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -74,6 +74,11 @@ jobs:
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - x86_64"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -93,7 +98,7 @@ jobs:
|
||||
*.sha256
|
||||
|
||||
macos-universal:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,50 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.1
|
||||
|
||||
This release includes support for range formatting (i.e., the ability to format specific lines
|
||||
within a source file).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurb`\] Implement `missing-f-string-syntax` (`RUF027`) ([#9728](https://github.com/astral-sh/ruff/pull/9728))
|
||||
- Format module-level docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Add `--range` option to `ruff format` ([#9733](https://github.com/astral-sh/ruff/pull/9733))
|
||||
- Don't trim last empty line in docstrings ([#9813](https://github.com/astral-sh/ruff/pull/9813))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Skip empty lines when determining base indentation ([#9795](https://github.com/astral-sh/ruff/pull/9795))
|
||||
- Drop `__get__` and `__set__` from `unnecessary-dunder-call` ([#9791](https://github.com/astral-sh/ruff/pull/9791))
|
||||
- Respect generic `Protocol` in ellipsis removal ([#9841](https://github.com/astral-sh/ruff/pull/9841))
|
||||
- Revert "Use publicly available Apple Silicon runners (#9726)" ([#9834](https://github.com/astral-sh/ruff/pull/9834))
|
||||
|
||||
### Performance
|
||||
|
||||
- Skip LibCST parsing for standard dedent adjustments ([#9769](https://github.com/astral-sh/ruff/pull/9769))
|
||||
- Remove CST-based fixer for `C408` ([#9822](https://github.com/astral-sh/ruff/pull/9822))
|
||||
- Add our own ignored-names abstractions ([#9802](https://github.com/astral-sh/ruff/pull/9802))
|
||||
- Remove CST-based fixers for `C400`, `C401`, `C410`, and `C418` ([#9819](https://github.com/astral-sh/ruff/pull/9819))
|
||||
- Use `AhoCorasick` to speed up quote match ([#9773](https://github.com/astral-sh/ruff/pull/9773))
|
||||
- Remove CST-based fixers for `C405` and `C409` ([#9821](https://github.com/astral-sh/ruff/pull/9821))
|
||||
- Add fast-path for comment detection ([#9808](https://github.com/astral-sh/ruff/pull/9808))
|
||||
- Invert order of checks in `zero-sleep-call` ([#9766](https://github.com/astral-sh/ruff/pull/9766))
|
||||
- Short-circuit typing matches based on imports ([#9800](https://github.com/astral-sh/ruff/pull/9800))
|
||||
- Run dunder method rule on methods directly ([#9815](https://github.com/astral-sh/ruff/pull/9815))
|
||||
- Track top-level module imports in the semantic model ([#9775](https://github.com/astral-sh/ruff/pull/9775))
|
||||
- Slight speed-up for lowercase and uppercase identifier checks ([#9798](https://github.com/astral-sh/ruff/pull/9798))
|
||||
- Remove LibCST-based fixer for `C403` ([#9818](https://github.com/astral-sh/ruff/pull/9818))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update `max-pos-args` example to `max-positional-args` ([#9797](https://github.com/astral-sh/ruff/pull/9797))
|
||||
- Fixed example code in `weak_cryptographic_key.rs` ([#9774](https://github.com/astral-sh/ruff/pull/9774))
|
||||
- Fix references to deprecated `ANN` rules in changelog ([#9771](https://github.com/astral-sh/ruff/pull/9771))
|
||||
- Fix default for `max-positional-args` ([#9838](https://github.com/astral-sh/ruff/pull/9838))
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Breaking changes
|
||||
@@ -13,8 +58,8 @@ See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
The following rules are now deprecated:
|
||||
|
||||
- [`missing-type-function-argument`](https://docs.astral.sh/ruff/rules/missing-type-function-argument/) (`ANN001`)
|
||||
- [`missing-type-args`](https://docs.astral.sh/ruff/rules/missing-type-args/) (`ANN002`)
|
||||
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
|
||||
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
|
||||
|
||||
The following command line options are now deprecated:
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ Once you've completed the code for the rule itself, you can define tests with th
|
||||
For example, if you're adding a new rule named `E402`, you would run:
|
||||
|
||||
```shell
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
|
||||
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
|
||||
```
|
||||
|
||||
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
|
||||
|
||||
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -1090,9 +1090,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75828adcb53122ef5ea649a39f50f82d94b754099bf6331b32e255e1891e8fb"
|
||||
checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
@@ -1132,9 +1132,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1324,9 +1324,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -1780,12 +1780,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyproject-toml"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
|
||||
checksum = "ef61ae096a2f8c8b49eca360679dbc25f57c99145f6634b6bc18fedb1f9c6c30"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"pep440_rs 0.3.12",
|
||||
"pep440_rs 0.4.0",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"toml",
|
||||
@@ -2005,7 +2005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2023,7 +2023,7 @@ dependencies = [
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"notify",
|
||||
@@ -2081,7 +2081,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"glob",
|
||||
"globset",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"regex",
|
||||
"ruff_macros",
|
||||
"seahash",
|
||||
@@ -2097,7 +2097,7 @@ dependencies = [
|
||||
"imara-diff",
|
||||
"indicatif",
|
||||
"indoc",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"libcst",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
@@ -2165,7 +2165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2181,7 +2181,7 @@ dependencies = [
|
||||
"insta",
|
||||
"is-macro",
|
||||
"is-wsl",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"libcst",
|
||||
"log",
|
||||
"memchr",
|
||||
@@ -2233,7 +2233,7 @@ dependencies = [
|
||||
name = "ruff_macros"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
@@ -2246,7 +2246,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"ruff_diagnostics",
|
||||
@@ -2264,10 +2264,12 @@ dependencies = [
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.4.1",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"once_cell",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
@@ -2298,7 +2300,7 @@ dependencies = [
|
||||
"clap",
|
||||
"countme",
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"regex",
|
||||
@@ -2341,7 +2343,7 @@ dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"hexf-parse",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"lexical-parse-float",
|
||||
"rand",
|
||||
"unic-ucd-category",
|
||||
@@ -2355,7 +2357,7 @@ dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"lalrpop",
|
||||
"lalrpop-util",
|
||||
"memchr",
|
||||
@@ -2406,7 +2408,7 @@ name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_index",
|
||||
"ruff_python_parser",
|
||||
@@ -2417,7 +2419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2488,7 +2490,7 @@ dependencies = [
|
||||
"globset",
|
||||
"ignore",
|
||||
"is-macro",
|
||||
"itertools 0.12.0",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
@@ -3058,9 +3060,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -3079,9 +3081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -47,15 +47,15 @@ indicatif ={ version = "0.17.7"}
|
||||
indoc ={ version = "2.0.4"}
|
||||
insta = { version = "1.34.0", feature = ["filters", "glob"] }
|
||||
insta-cmd = { version = "0.4.0" }
|
||||
is-macro = { version = "0.3.4" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.12.0" }
|
||||
itertools = { version = "0.12.1" }
|
||||
js-sys = { version = "0.3.67" }
|
||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
lexical-parse-float = { version = "0.8.0", features = ["format"] }
|
||||
libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = { version = "2.6.4" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version ="0.1.39"}
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
@@ -65,7 +65,7 @@ pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.4.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.78" }
|
||||
pyproject-toml = { version = "0.8.1" }
|
||||
pyproject-toml = { version = "0.8.2" }
|
||||
quick-junit = { version = "0.3.5" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
@@ -93,7 +93,7 @@ tempfile = { version ="3.9.0"}
|
||||
test-case = { version = "3.3.1" }
|
||||
thiserror = { version = "1.0.51" }
|
||||
tikv-jemallocator = { version ="0.5.0"}
|
||||
toml = { version = "0.8.8" }
|
||||
toml = { version = "0.8.9" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
@@ -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.2.0
|
||||
rev: v0.2.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -433,6 +433,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
|
||||
- [PyMC](https://github.com/pymc-devs/pymc/)
|
||||
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
|
||||
- [pytest](https://github.com/pytest-dev/pytest)
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
@@ -463,7 +464,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
|
||||
### Show Your Support
|
||||
|
||||
If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
|
||||
If you're using Ruff, consider adding the Ruff badge to your project's `README.md`:
|
||||
|
||||
```md
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
@@ -489,6 +490,6 @@ MIT
|
||||
|
||||
<div align="center">
|
||||
<a target="_blank" href="https://astral.sh" style="background:none">
|
||||
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg">
|
||||
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg" alt="Made by Astral">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::{command, Parser};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -12,6 +16,8 @@ use ruff_linter::settings::types::{
|
||||
SerializationFormat, UnsafeFixes,
|
||||
};
|
||||
use ruff_linter::{warn_user, RuleParser, RuleSelector, RuleSelectorParser};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::configuration::{Configuration, RuleSelection};
|
||||
use ruff_workspace::options::PycodestyleOptions;
|
||||
use ruff_workspace::resolver::ConfigurationTransformer;
|
||||
@@ -440,6 +446,21 @@ pub struct FormatCommand {
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
|
||||
/// When specified, Ruff will try to only format the code in the given range.
|
||||
/// It might be necessary to extend the start backwards or the end forwards, to fully enclose a logical line.
|
||||
/// The `<RANGE>` uses the format `<start_line>:<start_column>-<end_line>:<end_column>`.
|
||||
///
|
||||
/// - The line and column numbers are 1 based.
|
||||
/// - The column specifies the nth-unicode codepoint on that line.
|
||||
/// - The end offset is exclusive.
|
||||
/// - The column numbers are optional. You can write `--range=1-2` instead of `--range=1:1-2:1`.
|
||||
/// - The end position is optional. You can write `--range=2` to format the entire document starting from the second line.
|
||||
/// - The start position is optional. You can write `--range=-3` to format the first three lines of the document.
|
||||
///
|
||||
/// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported.
|
||||
#[clap(long, help_heading = "Editor options", verbatim_doc_comment)]
|
||||
pub range: Option<FormatRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||
@@ -570,6 +591,7 @@ impl FormatCommand {
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
stdin_filename: self.stdin_filename,
|
||||
range: self.range,
|
||||
},
|
||||
CliOverrides {
|
||||
line_length: self.line_length,
|
||||
@@ -670,6 +692,196 @@ pub struct FormatArguments {
|
||||
pub files: Vec<PathBuf>,
|
||||
pub isolated: bool,
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
pub range: Option<FormatRange>,
|
||||
}
|
||||
|
||||
/// A text range specified by line and column numbers.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct FormatRange {
|
||||
start: LineColumn,
|
||||
end: LineColumn,
|
||||
}
|
||||
|
||||
impl FormatRange {
|
||||
/// Converts the line:column range to a byte offset range specific for `source`.
|
||||
///
|
||||
/// Returns an empty range if the start range is past the end of `source`.
|
||||
pub(super) fn to_text_range(self, source: &str, line_index: &LineIndex) -> TextRange {
|
||||
let start_byte_offset = line_index.offset(self.start.line, self.start.column, source);
|
||||
let end_byte_offset = line_index.offset(self.end.line, self.end.column, source);
|
||||
|
||||
TextRange::new(start_byte_offset, end_byte_offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FormatRange {
|
||||
type Err = FormatRangeParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let (start, end) = value.split_once('-').unwrap_or((value, ""));
|
||||
|
||||
let start = if start.is_empty() {
|
||||
LineColumn::default()
|
||||
} else {
|
||||
start.parse().map_err(FormatRangeParseError::InvalidStart)?
|
||||
};
|
||||
|
||||
let end = if end.is_empty() {
|
||||
LineColumn {
|
||||
line: OneIndexed::MAX,
|
||||
column: OneIndexed::MAX,
|
||||
}
|
||||
} else {
|
||||
end.parse().map_err(FormatRangeParseError::InvalidEnd)?
|
||||
};
|
||||
|
||||
if start > end {
|
||||
return Err(FormatRangeParseError::StartGreaterThanEnd(start, end));
|
||||
}
|
||||
|
||||
Ok(FormatRange { start, end })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FormatRangeParseError {
|
||||
InvalidStart(LineColumnParseError),
|
||||
InvalidEnd(LineColumnParseError),
|
||||
|
||||
StartGreaterThanEnd(LineColumn, LineColumn),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FormatRangeParseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let tip = " tip:".bold().green();
|
||||
match self {
|
||||
FormatRangeParseError::StartGreaterThanEnd(start, end) => {
|
||||
write!(
|
||||
f,
|
||||
"the start position '{start_invalid}' is greater than the end position '{end_invalid}'.\n {tip} Try switching start and end: '{end}-{start}'",
|
||||
start_invalid=start.to_string().bold().yellow(),
|
||||
end_invalid=end.to_string().bold().yellow(),
|
||||
start=start.to_string().green().bold(),
|
||||
end=end.to_string().green().bold()
|
||||
)
|
||||
}
|
||||
FormatRangeParseError::InvalidStart(inner) => inner.write(f, true),
|
||||
FormatRangeParseError::InvalidEnd(inner) => inner.write(f, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FormatRangeParseError {}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct LineColumn {
|
||||
pub line: OneIndexed,
|
||||
pub column: OneIndexed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LineColumn {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{line}:{column}", line = self.line, column = self.column)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LineColumn {
|
||||
fn default() -> Self {
|
||||
LineColumn {
|
||||
line: OneIndexed::MIN,
|
||||
column: OneIndexed::MIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for LineColumn {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for LineColumn {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.line
|
||||
.cmp(&other.line)
|
||||
.then(self.column.cmp(&other.column))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LineColumn {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for LineColumn {}
|
||||
|
||||
impl FromStr for LineColumn {
|
||||
type Err = LineColumnParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let (line, column) = value.split_once(':').unwrap_or((value, "1"));
|
||||
|
||||
let line: usize = line.parse().map_err(LineColumnParseError::LineParseError)?;
|
||||
let column: usize = column
|
||||
.parse()
|
||||
.map_err(LineColumnParseError::ColumnParseError)?;
|
||||
|
||||
match (OneIndexed::new(line), OneIndexed::new(column)) {
|
||||
(Some(line), Some(column)) => Ok(LineColumn { line, column }),
|
||||
(Some(line), None) => Err(LineColumnParseError::ZeroColumnIndex { line }),
|
||||
(None, Some(column)) => Err(LineColumnParseError::ZeroLineIndex { column }),
|
||||
(None, None) => Err(LineColumnParseError::ZeroLineAndColumnIndex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LineColumnParseError {
|
||||
ZeroLineIndex { column: OneIndexed },
|
||||
ZeroColumnIndex { line: OneIndexed },
|
||||
ZeroLineAndColumnIndex,
|
||||
LineParseError(std::num::ParseIntError),
|
||||
ColumnParseError(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl LineColumnParseError {
|
||||
fn write(&self, f: &mut std::fmt::Formatter, start_range: bool) -> std::fmt::Result {
|
||||
let tip = "tip:".bold().green();
|
||||
|
||||
let range = if start_range { "start" } else { "end" };
|
||||
|
||||
match self {
|
||||
LineColumnParseError::ColumnParseError(inner) => {
|
||||
write!(f, "the {range}s column is not a valid number ({inner})'\n {tip} The format is 'line:column'.")
|
||||
}
|
||||
LineColumnParseError::LineParseError(inner) => {
|
||||
write!(f, "the {range} line is not a valid number ({inner})\n {tip} The format is 'line:column'.")
|
||||
}
|
||||
LineColumnParseError::ZeroColumnIndex { line } => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} column is 0, but it should be 1 or greater.\n {tip} The column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion=format!("{line}:1").green().bold()
|
||||
)
|
||||
}
|
||||
LineColumnParseError::ZeroLineIndex { column } => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} line is 0, but it should be 1 or greater.\n {tip} The line numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion=format!("1:{column}").green().bold()
|
||||
)
|
||||
}
|
||||
LineColumnParseError::ZeroLineAndColumnIndex => {
|
||||
write!(
|
||||
f,
|
||||
"the {range} line and column are both 0, but they should be 1 or greater.\n {tip} The line and column numbers start at 1.\n {tip} Try {suggestion} instead.",
|
||||
suggestion="1:1".to_string().green().bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI settings that function as configuration overrides.
|
||||
|
||||
@@ -1050,6 +1050,7 @@ mod tests {
|
||||
&self.settings.formatter,
|
||||
PySourceType::Python,
|
||||
FormatMode::Write,
|
||||
None,
|
||||
Some(cache),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,12 +23,13 @@ use ruff_linter::rules::flake8_quotes::settings::Quote;
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
|
||||
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle};
|
||||
use ruff_source_file::LineIndex;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
|
||||
use crate::panic::{catch_unwind, PanicError};
|
||||
use crate::resolve::resolve;
|
||||
@@ -77,6 +78,13 @@ pub(crate) fn format(
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if cli.range.is_some() && paths.len() > 1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
|
||||
paths.len()
|
||||
));
|
||||
}
|
||||
|
||||
warn_incompatible_formatter_settings(&resolver);
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
@@ -139,7 +147,14 @@ pub(crate) fn format(
|
||||
|
||||
Some(
|
||||
match catch_unwind(|| {
|
||||
format_path(path, &settings.formatter, source_type, mode, cache)
|
||||
format_path(
|
||||
path,
|
||||
&settings.formatter,
|
||||
source_type,
|
||||
mode,
|
||||
cli.range,
|
||||
cache,
|
||||
)
|
||||
}) {
|
||||
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||
path: resolved_file.path().to_path_buf(),
|
||||
@@ -226,6 +241,7 @@ pub(crate) fn format_path(
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
range: Option<FormatRange>,
|
||||
cache: Option<&Cache>,
|
||||
) -> Result<FormatResult, FormatCommandError> {
|
||||
if let Some(cache) = cache {
|
||||
@@ -250,8 +266,12 @@ pub(crate) fn format_path(
|
||||
}
|
||||
};
|
||||
|
||||
// Don't write back to the cache if formatting a range.
|
||||
let cache = cache.filter(|_| range.is_none());
|
||||
|
||||
// Format the source.
|
||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
|
||||
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)?
|
||||
{
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::Write => {
|
||||
let mut writer = File::create(path).map_err(|err| {
|
||||
@@ -319,12 +339,31 @@ pub(crate) fn format_source(
|
||||
source_type: PySourceType,
|
||||
path: Option<&Path>,
|
||||
settings: &FormatterSettings,
|
||||
range: Option<FormatRange>,
|
||||
) -> Result<FormattedSource, FormatCommandError> {
|
||||
match &source_kind {
|
||||
SourceKind::Python(unformatted) => {
|
||||
let options = settings.to_format_options(source_type, unformatted);
|
||||
|
||||
let formatted = format_module_source(unformatted, options).map_err(|err| {
|
||||
let formatted = if let Some(range) = range {
|
||||
let line_index = LineIndex::from_source_text(unformatted);
|
||||
let byte_range = range.to_text_range(unformatted, &line_index);
|
||||
format_range(unformatted, byte_range, options).map(|formatted_range| {
|
||||
let mut formatted = unformatted.to_string();
|
||||
formatted.replace_range(
|
||||
std::ops::Range::<usize>::from(formatted_range.source_range()),
|
||||
formatted_range.as_code(),
|
||||
);
|
||||
|
||||
formatted
|
||||
})
|
||||
} else {
|
||||
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
|
||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||
format_module_source(unformatted, options).map(|formatted| formatted.into_code())
|
||||
};
|
||||
|
||||
let formatted = formatted.map_err(|err| {
|
||||
if let FormatModuleError::ParseError(err) = err {
|
||||
DisplayParseError::from_source_kind(
|
||||
err,
|
||||
@@ -337,7 +376,6 @@ pub(crate) fn format_source(
|
||||
}
|
||||
})?;
|
||||
|
||||
let formatted = formatted.into_code();
|
||||
if formatted.len() == unformatted.len() && formatted == *unformatted {
|
||||
Ok(FormattedSource::Unchanged)
|
||||
} else {
|
||||
@@ -349,6 +387,12 @@ pub(crate) fn format_source(
|
||||
return Ok(FormattedSource::Unchanged);
|
||||
}
|
||||
|
||||
if range.is_some() {
|
||||
return Err(FormatCommandError::RangeFormatNotebook(
|
||||
path.map(Path::to_path_buf),
|
||||
));
|
||||
}
|
||||
|
||||
let options = settings.to_format_options(source_type, notebook.source_code());
|
||||
|
||||
let mut output: Option<String> = None;
|
||||
@@ -589,6 +633,7 @@ pub(crate) enum FormatCommandError {
|
||||
Format(Option<PathBuf>, FormatModuleError),
|
||||
Write(Option<PathBuf>, SourceError),
|
||||
Diff(Option<PathBuf>, io::Error),
|
||||
RangeFormatNotebook(Option<PathBuf>),
|
||||
}
|
||||
|
||||
impl FormatCommandError {
|
||||
@@ -606,7 +651,8 @@ impl FormatCommandError {
|
||||
| Self::Read(path, _)
|
||||
| Self::Format(path, _)
|
||||
| Self::Write(path, _)
|
||||
| Self::Diff(path, _) => path.as_deref(),
|
||||
| Self::Diff(path, _)
|
||||
| Self::RangeFormatNotebook(path) => path.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -628,9 +674,10 @@ impl Display for FormatCommandError {
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
"Encountered error:".bold(),
|
||||
err.io_error()
|
||||
"{header} {error}",
|
||||
header = "Encountered error:".bold(),
|
||||
error = err
|
||||
.io_error()
|
||||
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
|
||||
)
|
||||
}
|
||||
@@ -648,7 +695,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to read:".bold())
|
||||
}
|
||||
}
|
||||
Self::Write(path, err) => {
|
||||
@@ -661,7 +708,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to write:".bold())
|
||||
}
|
||||
}
|
||||
Self::Format(path, err) => {
|
||||
@@ -674,7 +721,7 @@ impl Display for FormatCommandError {
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
|
||||
write!(f, "{header} {err}", header = "Failed to format:".bold())
|
||||
}
|
||||
}
|
||||
Self::Diff(path, err) => {
|
||||
@@ -689,9 +736,25 @@ impl Display for FormatCommandError {
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}{} {err}",
|
||||
"Failed to generate diff".bold(),
|
||||
":".bold()
|
||||
"{header} {err}",
|
||||
header = "Failed to generate diff:".bold(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::RangeFormatNotebook(path) => {
|
||||
if let Some(path) = path {
|
||||
write!(
|
||||
f,
|
||||
"{header}{path}{colon} Range formatting isn't supported for notebooks.",
|
||||
header = "Failed to format ".bold(),
|
||||
path = fs::relativize_path(path).bold(),
|
||||
colon = ":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{header} Range formatting isn't supported for notebooks",
|
||||
header = "Failed to format:".bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::args::{CliOverrides, FormatArguments, FormatRange};
|
||||
use crate::commands::format::{
|
||||
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
|
||||
FormatResult, FormattedSource,
|
||||
@@ -69,7 +69,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
};
|
||||
|
||||
// Format the file.
|
||||
match format_source_code(path, settings, source_type, mode) {
|
||||
match format_source_code(path, cli.range, settings, source_type, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
@@ -90,6 +90,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
/// Format source code read from `stdin`.
|
||||
fn format_source_code(
|
||||
path: Option<&Path>,
|
||||
range: Option<FormatRange>,
|
||||
settings: &FormatterSettings,
|
||||
source_type: PySourceType,
|
||||
mode: FormatMode,
|
||||
@@ -107,7 +108,7 @@ fn format_source_code(
|
||||
};
|
||||
|
||||
// Format the source.
|
||||
let formatted = format_source(&source_kind, source_type, path, settings)?;
|
||||
let formatted = format_source(&source_kind, source_type, path, settings, range)?;
|
||||
|
||||
match &formatted {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
|
||||
@@ -1544,3 +1544,322 @@ include = ["*.ipy"]
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:8-2:14"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_unicode() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2:21-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1="👋🏽" ): print("Format this" )
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1="👋🏽" ):
|
||||
print("Format this")
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_multiple_files() -> std::io::Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let file1 = tempdir.path().join("file1.py");
|
||||
|
||||
fs::write(
|
||||
&file1,
|
||||
r#"
|
||||
def file1(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let file2 = tempdir.path().join("file2.py");
|
||||
|
||||
fs::write(
|
||||
&file2,
|
||||
r#"
|
||||
def file2(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--range=1:8-1:15"])
|
||||
.arg(file1)
|
||||
.arg(file2), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: The `--range` option is only supported when formatting a single file but the specified paths resolve to 2 files.
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_out_of_bounds() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=100:40-200:1"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_start_larger_than_end() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=90-50"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '90-50' for '--range <RANGE>': the start position '90:1' is greater than the end position '50:1'.
|
||||
tip: Try switching start and end: '50:1-90:1'
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_line_numbers_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=2-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Shouldn't format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_start_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this")
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_end_only() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=-3"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
def foo(
|
||||
arg1,
|
||||
arg2,
|
||||
):
|
||||
print("Should format this" )
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_missing_line() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=1-:20"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '1-:20' for '--range <RANGE>': the end line is not a valid number (cannot parse integer from empty string)
|
||||
tip: The format is 'line:column'.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_line_number() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:2"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '0:2' for '--range <RANGE>': the start line is 0, but it should be 1 or greater.
|
||||
tip: The line numbers start at 1.
|
||||
tip: Try 1:2 instead.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_and_line_zero() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py", "--range=0:0"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Should format this" )
|
||||
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value '0:0' for '--range <RANGE>': the start line and column are both 0, but they should be 1 or greater.
|
||||
tip: The line and column numbers start at 1.
|
||||
tip: Try 1:1 instead.
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_formatting_notebook() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated", "--no-cache", "--stdin-filename", "main.ipynb", "--range=1-2"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x=1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
"#), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -308,11 +308,8 @@ impl std::fmt::Debug for Token {
|
||||
/// assert_eq!(printed.as_code(), r#""Hello 'Ruff'""#);
|
||||
/// assert_eq!(printed.sourcemap(), [
|
||||
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(0) },
|
||||
/// SourceMarker { source: TextSize::new(0), dest: TextSize::new(7) },
|
||||
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(7) },
|
||||
/// SourceMarker { source: TextSize::new(8), dest: TextSize::new(13) },
|
||||
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(13) },
|
||||
/// SourceMarker { source: TextSize::new(14), dest: TextSize::new(14) },
|
||||
/// SourceMarker { source: TextSize::new(20), dest: TextSize::new(14) },
|
||||
/// ]);
|
||||
///
|
||||
@@ -340,18 +337,18 @@ impl<Context> Format<Context> for SourcePosition {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a text from a dynamic string with its optional start-position in the source document.
|
||||
/// Creates a text from a dynamic string.
|
||||
///
|
||||
/// This is done by allocating a new string internally.
|
||||
pub fn text(text: &str, position: Option<TextSize>) -> Text {
|
||||
pub fn text(text: &str) -> Text {
|
||||
debug_assert_no_newlines(text);
|
||||
|
||||
Text { text, position }
|
||||
Text { text }
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct Text<'a> {
|
||||
text: &'a str,
|
||||
position: Option<TextSize>,
|
||||
}
|
||||
|
||||
impl<Context> Format<Context> for Text<'_>
|
||||
@@ -359,10 +356,6 @@ where
|
||||
Context: FormatContext,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
|
||||
if let Some(position) = self.position {
|
||||
source_position(position).fmt(f)?;
|
||||
}
|
||||
|
||||
f.write_element(FormatElement::Text {
|
||||
text: self.text.to_string().into_boxed_str(),
|
||||
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
|
||||
@@ -2292,7 +2285,7 @@ impl<Context, T> std::fmt::Debug for FormatWith<Context, T> {
|
||||
/// let mut join = f.join_with(&separator);
|
||||
///
|
||||
/// for item in &self.items {
|
||||
/// join.entry(&format_with(|f| write!(f, [text(item, None)])));
|
||||
/// join.entry(&format_with(|f| write!(f, [text(item)])));
|
||||
/// }
|
||||
/// join.finish()
|
||||
/// })),
|
||||
@@ -2377,7 +2370,7 @@ where
|
||||
/// let mut count = 0;
|
||||
///
|
||||
/// let value = format_once(|f| {
|
||||
/// write!(f, [text(&std::format!("Formatted {count}."), None)])
|
||||
/// write!(f, [text(&std::format!("Formatted {count}."))])
|
||||
/// });
|
||||
///
|
||||
/// format!(SimpleFormatContext::default(), [value]).expect("Formatting once works fine");
|
||||
|
||||
@@ -346,10 +346,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
}
|
||||
|
||||
FormatElement::SourcePosition(position) => {
|
||||
write!(
|
||||
f,
|
||||
[text(&std::format!("source_position({position:?})"), None)]
|
||||
)?;
|
||||
write!(f, [text(&std::format!("source_position({position:?})"))])?;
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
@@ -360,7 +357,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
write!(f, [token("best_fitting(")])?;
|
||||
|
||||
if *mode != BestFittingMode::FirstLine {
|
||||
write!(f, [text(&std::format!("mode: {mode:?}, "), None)])?;
|
||||
write!(f, [text(&std::format!("mode: {mode:?}, "))])?;
|
||||
}
|
||||
|
||||
write!(f, [token("[")])?;
|
||||
@@ -392,17 +389,14 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("<interned {index}>"), None),
|
||||
text(&std::format!("<interned {index}>")),
|
||||
space(),
|
||||
&&**interned,
|
||||
]
|
||||
)?;
|
||||
}
|
||||
Some(reference) => {
|
||||
write!(
|
||||
f,
|
||||
[text(&std::format!("<ref interned *{reference}>"), None)]
|
||||
)?;
|
||||
write!(f, [text(&std::format!("<ref interned *{reference}>"))])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +415,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("<END_TAG_WITHOUT_START<"),
|
||||
text(&std::format!("{:?}", tag.kind()), None),
|
||||
text(&std::format!("{:?}", tag.kind())),
|
||||
token(">>"),
|
||||
]
|
||||
)?;
|
||||
@@ -436,9 +430,9 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
token(")"),
|
||||
soft_line_break_or_space(),
|
||||
token("ERROR<START_END_TAG_MISMATCH<start: "),
|
||||
text(&std::format!("{start_kind:?}"), None),
|
||||
text(&std::format!("{start_kind:?}")),
|
||||
token(", end: "),
|
||||
text(&std::format!("{:?}", tag.kind()), None),
|
||||
text(&std::format!("{:?}", tag.kind())),
|
||||
token(">>")
|
||||
]
|
||||
)?;
|
||||
@@ -470,7 +464,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("align("),
|
||||
text(&count.to_string(), None),
|
||||
text(&count.to_string()),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -482,7 +476,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("line_suffix("),
|
||||
text(&std::format!("{reserved_width:?}"), None),
|
||||
text(&std::format!("{reserved_width:?}")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -499,11 +493,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = group.id() {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -524,11 +514,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = id {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space(),]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -561,7 +547,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("indent_if_group_breaks("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -581,11 +567,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
if let Some(group_id) = condition.group_id {
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&std::format!("\"{group_id:?}\""), None),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
[text(&std::format!("\"{group_id:?}\"")), token(","), space()]
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -595,7 +577,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
f,
|
||||
[
|
||||
token("label("),
|
||||
text(&std::format!("\"{label_id:?}\""), None),
|
||||
text(&std::format!("\"{label_id:?}\"")),
|
||||
token(","),
|
||||
space(),
|
||||
]
|
||||
@@ -664,7 +646,7 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
|
||||
ContentArrayEnd,
|
||||
token(")"),
|
||||
soft_line_break_or_space(),
|
||||
text(&std::format!("<START_WITHOUT_END<{top:?}>>"), None),
|
||||
text(&std::format!("<START_WITHOUT_END<{top:?}>>")),
|
||||
]
|
||||
)?;
|
||||
}
|
||||
@@ -807,7 +789,7 @@ impl Format<IrFormatContext<'_>> for Condition {
|
||||
f,
|
||||
[
|
||||
token("if_group_fits_on_line("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(")")
|
||||
]
|
||||
),
|
||||
@@ -816,7 +798,7 @@ impl Format<IrFormatContext<'_>> for Condition {
|
||||
f,
|
||||
[
|
||||
token("if_group_breaks("),
|
||||
text(&std::format!("\"{id:?}\""), None),
|
||||
text(&std::format!("\"{id:?}\"")),
|
||||
token(")")
|
||||
]
|
||||
),
|
||||
|
||||
@@ -32,7 +32,7 @@ pub trait MemoizeFormat<Context> {
|
||||
/// let value = self.value.get();
|
||||
/// self.value.set(value + 1);
|
||||
///
|
||||
/// write!(f, [text(&std::format!("Formatted {value} times."), None)])
|
||||
/// write!(f, [text(&std::format!("Formatted {value} times."))])
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -110,7 +110,7 @@ where
|
||||
/// write!(f, [
|
||||
/// token("Count:"),
|
||||
/// space(),
|
||||
/// text(&std::format!("{current}"), None),
|
||||
/// text(&std::format!("{current}")),
|
||||
/// hard_line_break()
|
||||
/// ])?;
|
||||
///
|
||||
|
||||
@@ -41,7 +41,7 @@ use std::marker::PhantomData;
|
||||
use std::num::{NonZeroU16, NonZeroU8, TryFromIntError};
|
||||
|
||||
use crate::format_element::document::Document;
|
||||
use crate::printer::{Printer, PrinterOptions, SourceMapGeneration};
|
||||
use crate::printer::{Printer, PrinterOptions};
|
||||
pub use arguments::{Argument, Arguments};
|
||||
pub use buffer::{
|
||||
Buffer, BufferExtensions, BufferSnapshot, Inspect, RemoveSoftLinesBuffer, VecBuffer,
|
||||
@@ -269,7 +269,6 @@ impl FormatOptions for SimpleFormatOptions {
|
||||
line_width: self.line_width,
|
||||
indent_style: self.indent_style,
|
||||
indent_width: self.indent_width,
|
||||
source_map_generation: SourceMapGeneration::Enabled,
|
||||
..PrinterOptions::default()
|
||||
}
|
||||
}
|
||||
@@ -433,28 +432,40 @@ impl Printed {
|
||||
std::mem::take(&mut self.verbatim_ranges)
|
||||
}
|
||||
|
||||
/// Slices the formatted code to the sub-slices that covers the passed `source_range`.
|
||||
/// Slices the formatted code to the sub-slices that covers the passed `source_range` in `source`.
|
||||
///
|
||||
/// The implementation uses the source map generated during formatting to find the closest range
|
||||
/// in the formatted document that covers `source_range` or more. The returned slice
|
||||
/// matches the `source_range` exactly (except indent, see below) if the formatter emits [`FormatElement::SourcePosition`] for
|
||||
/// the range's offsets.
|
||||
///
|
||||
/// ## Indentation
|
||||
/// The indentation before `source_range.start` is replaced with the indentation returned by the formatter
|
||||
/// to fix up incorrectly intended code.
|
||||
///
|
||||
/// Returns the entire document if the source map is empty.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `source_range` points to offsets that are not in the bounds of `source`.
|
||||
#[must_use]
|
||||
pub fn slice_range(self, source_range: TextRange) -> PrintedRange {
|
||||
pub fn slice_range(self, source_range: TextRange, source: &str) -> PrintedRange {
|
||||
let mut start_marker: Option<SourceMarker> = None;
|
||||
let mut end_marker: Option<SourceMarker> = None;
|
||||
|
||||
// Note: The printer can generate multiple source map entries for the same source position.
|
||||
// For example if you have:
|
||||
// * token("a + b")
|
||||
// * `source_position(276)`
|
||||
// * `token("def")`
|
||||
// * `token("foo")`
|
||||
// * `source_position(284)`
|
||||
// The printer uses the source position 276 for both the tokens `def` and `foo` because that's the only position it knows of.
|
||||
// * `token(")")`
|
||||
// * `source_position(276)`
|
||||
// * `hard_line_break`
|
||||
// The printer uses the source position 276 for both the tokens `)` and the `\n` because
|
||||
// there were multiple `source_position` entries in the IR with the same offset.
|
||||
// This can happen if multiple nodes start or end at the same position. A common example
|
||||
// for this are expressions and expression statement that always end at the same offset.
|
||||
//
|
||||
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed.
|
||||
// Warning: Source markers are often emitted sorted by their source position but it's not guaranteed
|
||||
// and depends on the emitted `IR`.
|
||||
// They are only guaranteed to be sorted in increasing order by their destination position.
|
||||
for marker in self.sourcemap {
|
||||
// Take the closest start marker, but skip over start_markers that have the same start.
|
||||
@@ -471,17 +482,44 @@ impl Printed {
|
||||
}
|
||||
}
|
||||
|
||||
let start = start_marker.map(|marker| marker.dest).unwrap_or_default();
|
||||
let end = end_marker.map_or_else(|| self.code.text_len(), |marker| marker.dest);
|
||||
let code_range = TextRange::new(start, end);
|
||||
let (source_start, formatted_start) = start_marker
|
||||
.map(|marker| (marker.source, marker.dest))
|
||||
.unwrap_or_default();
|
||||
|
||||
let (source_end, formatted_end) = end_marker
|
||||
.map_or((source.text_len(), self.code.text_len()), |marker| {
|
||||
(marker.source, marker.dest)
|
||||
});
|
||||
|
||||
let source_range = TextRange::new(source_start, source_end);
|
||||
let formatted_range = TextRange::new(formatted_start, formatted_end);
|
||||
|
||||
// Extend both ranges to include the indentation
|
||||
let source_range = extend_range_to_include_indent(source_range, source);
|
||||
let formatted_range = extend_range_to_include_indent(formatted_range, &self.code);
|
||||
|
||||
PrintedRange {
|
||||
code: self.code[code_range].to_string(),
|
||||
code: self.code[formatted_range].to_string(),
|
||||
source_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends `range` backwards (by reducing `range.start`) to include any directly preceding whitespace (`\t` or ` `).
|
||||
///
|
||||
/// # Panics
|
||||
/// If `range.start` is out of `source`'s bounds.
|
||||
fn extend_range_to_include_indent(range: TextRange, source: &str) -> TextRange {
|
||||
let whitespace_len: TextSize = source[..usize::from(range.start())]
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| matches!(c, ' ' | '\t'))
|
||||
.map(TextLen::text_len)
|
||||
.sum();
|
||||
|
||||
TextRange::new(range.start() - whitespace_len, range.end())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -537,7 +575,7 @@ pub type FormatResult<F> = Result<F, FormatError>;
|
||||
/// impl Format<SimpleFormatContext> for Paragraph {
|
||||
/// fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
|
||||
/// write!(f, [
|
||||
/// text(&self.0, None),
|
||||
/// text(&self.0),
|
||||
/// hard_line_break(),
|
||||
/// ])
|
||||
/// }
|
||||
|
||||
@@ -4,7 +4,7 @@ use drop_bomb::DebugDropBomb;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
pub use printer_options::*;
|
||||
use ruff_text_size::{Ranged, TextLen, TextSize};
|
||||
use ruff_text_size::{TextLen, TextSize};
|
||||
|
||||
use crate::format_element::document::Document;
|
||||
use crate::format_element::tag::{Condition, GroupMode};
|
||||
@@ -76,6 +76,9 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Push any pending marker
|
||||
self.push_marker();
|
||||
|
||||
Ok(Printed::new(
|
||||
self.state.buffer,
|
||||
None,
|
||||
@@ -97,42 +100,38 @@ impl<'a> Printer<'a> {
|
||||
let args = stack.top();
|
||||
|
||||
match element {
|
||||
FormatElement::Space => self.print_text(Text::Token(" "), None),
|
||||
FormatElement::Token { text } => self.print_text(Text::Token(text), None),
|
||||
FormatElement::Text { text, text_width } => self.print_text(
|
||||
Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
},
|
||||
None,
|
||||
),
|
||||
FormatElement::Space => self.print_text(Text::Token(" ")),
|
||||
FormatElement::Token { text } => self.print_text(Text::Token(text)),
|
||||
FormatElement::Text { text, text_width } => self.print_text(Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
}),
|
||||
FormatElement::SourceCodeSlice { slice, text_width } => {
|
||||
let text = slice.text(self.source_code);
|
||||
self.print_text(
|
||||
Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
},
|
||||
Some(slice.range()),
|
||||
);
|
||||
self.print_text(Text::Text {
|
||||
text,
|
||||
text_width: *text_width,
|
||||
});
|
||||
}
|
||||
FormatElement::Line(line_mode) => {
|
||||
if args.mode().is_flat()
|
||||
&& matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace)
|
||||
{
|
||||
if line_mode == &LineMode::SoftOrSpace {
|
||||
self.print_text(Text::Token(" "), None);
|
||||
self.print_text(Text::Token(" "));
|
||||
}
|
||||
} else if self.state.line_suffixes.has_pending() {
|
||||
self.flush_line_suffixes(queue, stack, Some(element));
|
||||
} else {
|
||||
// Only print a newline if the current line isn't already empty
|
||||
if self.state.line_width > 0 {
|
||||
self.push_marker();
|
||||
self.print_char('\n');
|
||||
}
|
||||
|
||||
// Print a second line break if this is an empty line
|
||||
if line_mode == &LineMode::Empty {
|
||||
self.push_marker();
|
||||
self.print_char('\n');
|
||||
}
|
||||
|
||||
@@ -145,14 +144,11 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
|
||||
FormatElement::SourcePosition(position) => {
|
||||
self.state.source_position = *position;
|
||||
// The printer defers printing indents until the next text
|
||||
// is printed. Pushing the marker now would mean that the
|
||||
// mapped range includes the indent range, which we don't want.
|
||||
// Only add a marker if we're not in an indented context, e.g. at the end of the file.
|
||||
if self.state.pending_indent.is_empty() {
|
||||
self.push_marker();
|
||||
}
|
||||
// Queue the source map position and emit it when printing the next character
|
||||
self.state.pending_source_position = Some(*position);
|
||||
}
|
||||
|
||||
FormatElement::LineSuffixBoundary => {
|
||||
@@ -444,7 +440,7 @@ impl<'a> Printer<'a> {
|
||||
Ok(print_mode)
|
||||
}
|
||||
|
||||
fn print_text(&mut self, text: Text, source_range: Option<TextRange>) {
|
||||
fn print_text(&mut self, text: Text) {
|
||||
if !self.state.pending_indent.is_empty() {
|
||||
let (indent_char, repeat_count) = match self.options.indent_style() {
|
||||
IndentStyle::Tab => ('\t', 1),
|
||||
@@ -467,19 +463,6 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Insert source map markers before and after the token
|
||||
//
|
||||
// If the token has source position information the start marker
|
||||
// will use the start position of the original token, and the end
|
||||
// marker will use that position + the text length of the token
|
||||
//
|
||||
// If the token has no source position (was created by the formatter)
|
||||
// both the start and end marker will use the last known position
|
||||
// in the input source (from state.source_position)
|
||||
if let Some(range) = source_range {
|
||||
self.state.source_position = range.start();
|
||||
}
|
||||
|
||||
self.push_marker();
|
||||
|
||||
match text {
|
||||
@@ -502,21 +485,15 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(range) = source_range {
|
||||
self.state.source_position = range.end();
|
||||
}
|
||||
|
||||
self.push_marker();
|
||||
}
|
||||
|
||||
fn push_marker(&mut self) {
|
||||
if self.options.source_map_generation.is_disabled() {
|
||||
let Some(source_position) = self.state.pending_source_position.take() else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let marker = SourceMarker {
|
||||
source: self.state.source_position,
|
||||
source: source_position,
|
||||
dest: self.state.buffer.text_len(),
|
||||
};
|
||||
|
||||
@@ -897,7 +874,7 @@ enum FillPairLayout {
|
||||
struct PrinterState<'a> {
|
||||
buffer: String,
|
||||
source_markers: Vec<SourceMarker>,
|
||||
source_position: TextSize,
|
||||
pending_source_position: Option<TextSize>,
|
||||
pending_indent: Indention,
|
||||
measured_group_fits: bool,
|
||||
line_width: u32,
|
||||
@@ -1752,7 +1729,7 @@ a",
|
||||
let result = format_with_options(
|
||||
&format_args![
|
||||
token("function main() {"),
|
||||
block_indent(&text("let x = `This is a multiline\nstring`;", None)),
|
||||
block_indent(&text("let x = `This is a multiline\nstring`;")),
|
||||
token("}"),
|
||||
hard_line_break()
|
||||
],
|
||||
@@ -1769,7 +1746,7 @@ a",
|
||||
fn it_breaks_a_group_if_a_string_contains_a_newline() {
|
||||
let result = format(&FormatArrayElements {
|
||||
items: vec![
|
||||
&text("`This is a string spanning\ntwo lines`", None),
|
||||
&text("`This is a string spanning\ntwo lines`"),
|
||||
&token("\"b\""),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,10 +14,6 @@ pub struct PrinterOptions {
|
||||
|
||||
/// The type of line ending to apply to the printed input
|
||||
pub line_ending: LineEnding,
|
||||
|
||||
/// Whether the printer should build a source map that allows mapping positions in the source document
|
||||
/// to positions in the formatted document.
|
||||
pub source_map_generation: SourceMapGeneration,
|
||||
}
|
||||
|
||||
impl<'a, O> From<&'a O> for PrinterOptions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -13,3 +13,11 @@ s = f"{set([f(x) for x in 'ab'])}"
|
||||
|
||||
s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
|
||||
s = set( # comment
|
||||
[x for x in range(3)]
|
||||
)
|
||||
|
||||
s = set([ # comment
|
||||
x for x in range(3)
|
||||
])
|
||||
|
||||
@@ -20,3 +20,10 @@ f"{dict(x='y') | dict(y='z')}"
|
||||
f"{ dict(x='y') | dict(y='z') }"
|
||||
f"a {dict(x='y') | dict(y='z')} b"
|
||||
f"a { dict(x='y') | dict(y='z') } b"
|
||||
|
||||
dict(
|
||||
# comment
|
||||
)
|
||||
|
||||
tuple( # comment
|
||||
)
|
||||
|
||||
@@ -8,3 +8,11 @@ t4 = tuple([
|
||||
t5 = tuple(
|
||||
(1, 2)
|
||||
)
|
||||
|
||||
tuple( # comment
|
||||
[1, 2]
|
||||
)
|
||||
|
||||
tuple([ # comment
|
||||
1, 2
|
||||
])
|
||||
|
||||
@@ -2,3 +2,12 @@ l1 = list([1, 2])
|
||||
l2 = list((1, 2))
|
||||
l3 = list([])
|
||||
l4 = list(())
|
||||
|
||||
|
||||
list( # comment
|
||||
[1, 2]
|
||||
)
|
||||
|
||||
list([ # comment
|
||||
1, 2
|
||||
])
|
||||
|
||||
@@ -207,3 +207,23 @@ class Repro:
|
||||
def stub(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
|
||||
class Repro(Protocol[int]):
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
|
||||
class Repro[int](Protocol):
|
||||
def func(self) -> str:
|
||||
"""Docstring"""
|
||||
...
|
||||
|
||||
def impl(self) -> str:
|
||||
"""Docstring"""
|
||||
return self.func()
|
||||
|
||||
@@ -198,3 +198,43 @@ else:
|
||||
def sb(self):
|
||||
if self._sb is not None: return self._sb
|
||||
else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
|
||||
def indent(x, y, w, z):
|
||||
if x: # [no-else-return]
|
||||
a = 1
|
||||
return y
|
||||
else:
|
||||
# comment
|
||||
c = 3
|
||||
return z
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
'''trailing whitespace
|
||||
inside a multiline string'''
|
||||
|
||||
f'''trailing whitespace
|
||||
inside a multiline f-string'''
|
||||
|
||||
# Trailing whitespace after `{`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
|
||||
# Trailing whitespace after `2`
|
||||
f'abc {
|
||||
1 + 2
|
||||
}'
|
||||
|
||||
@@ -30,6 +30,11 @@ class Thing:
|
||||
def do_thing(self, item):
|
||||
return object.__getattribute__(self, item) # PLC2801
|
||||
|
||||
def use_descriptor(self, item):
|
||||
item.__get__(self, type(self)) # OK
|
||||
item.__set__(self, 1) # OK
|
||||
item.__delete__(self) # OK
|
||||
|
||||
|
||||
blah = lambda: {"a": 1}.__delitem__("a") # OK
|
||||
|
||||
|
||||
@@ -215,3 +215,13 @@ if sys.version_info[:2] > (3,13):
|
||||
|
||||
if sys.version_info[:3] > (3,13):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info > (3,0):
|
||||
f"this is\
|
||||
allowed too"
|
||||
|
||||
f"""the indentation on
|
||||
this line is significant"""
|
||||
|
||||
"this is\
|
||||
allowed too"
|
||||
|
||||
85
crates/ruff_linter/resources/test/fixtures/ruff/RUF027.py
vendored
Normal file
85
crates/ruff_linter/resources/test/fixtures/ruff/RUF027.py
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
val = 2
|
||||
|
||||
def simple_cases():
|
||||
a = 4
|
||||
b = "{a}" # RUF027
|
||||
c = "{a} {b} f'{val}' " # RUF027
|
||||
|
||||
def escaped_string():
|
||||
a = 4
|
||||
b = "escaped string: {{ brackets surround me }}" # RUF027
|
||||
|
||||
def raw_string():
|
||||
a = 4
|
||||
b = r"raw string with formatting: {a}" # RUF027
|
||||
c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
|
||||
|
||||
def print_name(name: str):
|
||||
a = 4
|
||||
print("Hello, {name}!") # RUF027
|
||||
print("The test value we're using today is {a}") # RUF027
|
||||
|
||||
def do_nothing(a):
|
||||
return a
|
||||
|
||||
def nested_funcs():
|
||||
a = 4
|
||||
print(do_nothing(do_nothing("{a}"))) # RUF027
|
||||
|
||||
def tripled_quoted():
|
||||
a = 4
|
||||
c = a
|
||||
single_line = """ {a} """ # RUF027
|
||||
# RUF027
|
||||
multi_line = a = """b { # comment
|
||||
c} d
|
||||
"""
|
||||
|
||||
def single_quoted_multi_line():
|
||||
a = 4
|
||||
# RUF027
|
||||
b = " {\
|
||||
a} \
|
||||
"
|
||||
|
||||
def implicit_concat():
|
||||
a = 4
|
||||
b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
|
||||
print(f"{a}" "{a}" f"{b}") # RUF027
|
||||
|
||||
def escaped_chars():
|
||||
a = 4
|
||||
b = "\"not escaped:\" \'{a}\' \"escaped:\": \'{{c}}\'" # RUF027
|
||||
|
||||
def alternative_formatter(src, **kwargs):
|
||||
src.format(**kwargs)
|
||||
|
||||
def format2(src, *args):
|
||||
pass
|
||||
|
||||
# These should not cause an RUF027 message
|
||||
def negative_cases():
|
||||
a = 4
|
||||
positive = False
|
||||
"""{a}"""
|
||||
"don't format: {a}"
|
||||
c = """ {b} """
|
||||
d = "bad variable: {invalid}"
|
||||
e = "incorrect syntax: {}"
|
||||
json = "{ positive: false }"
|
||||
json2 = "{ 'positive': false }"
|
||||
json3 = "{ 'positive': 'false' }"
|
||||
alternative_formatter("{a}", a = 5)
|
||||
formatted = "{a}".fmt(a = 7)
|
||||
print(do_nothing("{a}".format(a=3)))
|
||||
print(do_nothing(alternative_formatter("{a}", a = 5)))
|
||||
print(format(do_nothing("{a}"), a = 5))
|
||||
print("{a}".to_upper())
|
||||
print(do_nothing("{a}").format(a = "Test"))
|
||||
print(do_nothing("{a}").format2(a))
|
||||
|
||||
a = 4
|
||||
|
||||
"always ignore this: {a}"
|
||||
|
||||
print("but don't ignore this: {val}") # RUF027
|
||||
@@ -1,13 +1,15 @@
|
||||
use ruff_python_ast::str::raw_contents_range;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_python_semantic::{BindingKind, ContextualizedDefinition, Export};
|
||||
use ruff_python_semantic::{
|
||||
BindingKind, ContextualizedDefinition, Definition, Export, Member, MemberKind,
|
||||
};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::docstrings::Docstring;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle};
|
||||
use crate::rules::{flake8_annotations, flake8_pyi, pydocstyle, pylint};
|
||||
use crate::{docstrings, warn_user};
|
||||
|
||||
/// Run lint rules over all [`Definition`] nodes in the [`SemanticModel`].
|
||||
@@ -31,6 +33,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
]);
|
||||
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
|
||||
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
|
||||
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName);
|
||||
let enforce_docstrings = checker.any_enabled(&[
|
||||
Rule::BlankLineAfterLastSection,
|
||||
Rule::BlankLineAfterSummary,
|
||||
@@ -80,7 +83,12 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
Rule::UndocumentedPublicPackage,
|
||||
]);
|
||||
|
||||
if !enforce_annotations && !enforce_docstrings && !enforce_stubs && !enforce_stubs_and_runtime {
|
||||
if !enforce_annotations
|
||||
&& !enforce_docstrings
|
||||
&& !enforce_stubs
|
||||
&& !enforce_stubs_and_runtime
|
||||
&& !enforce_dunder_method
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,6 +155,19 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
|
||||
// pylint
|
||||
if enforce_dunder_method {
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
if let Definition::Member(Member {
|
||||
kind: MemberKind::Method(method),
|
||||
..
|
||||
}) = definition
|
||||
{
|
||||
pylint::rules::bad_dunder_method_name(checker, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pydocstyle
|
||||
if enforce_docstrings {
|
||||
if pydocstyle::helpers::should_ignore_definition(
|
||||
|
||||
@@ -319,7 +319,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
numpy::rules::numpy_2_0_deprecation(checker, expr);
|
||||
}
|
||||
if checker.enabled(Rule::DeprecatedMockImport) {
|
||||
pyupgrade::rules::deprecated_mock_attribute(checker, expr);
|
||||
pyupgrade::rules::deprecated_mock_attribute(checker, attribute);
|
||||
}
|
||||
if checker.enabled(Rule::SixPY3) {
|
||||
flake8_2020::rules::name_or_attribute(checker, expr);
|
||||
@@ -336,7 +336,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UndocumentedWarn) {
|
||||
flake8_logging::rules::undocumented_warn(checker, expr);
|
||||
}
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
if checker.enabled(Rule::PandasUseOfDotValues) {
|
||||
pandas_vet::rules::attr(checker, attribute);
|
||||
}
|
||||
}
|
||||
Expr::Call(
|
||||
call @ ast::ExprCall {
|
||||
@@ -636,14 +638,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_bandit::rules::tarfile_unsafe_members(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorList) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_list(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_generator_list(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorSet) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_generator_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryGeneratorDict) {
|
||||
flake8_comprehensions::rules::unnecessary_generator_dict(
|
||||
@@ -651,9 +649,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListComprehensionSet) {
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListComprehensionDict) {
|
||||
flake8_comprehensions::rules::unnecessary_list_comprehension_dict(
|
||||
@@ -661,9 +657,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralSet) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_set(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_set(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralDict) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_dict(
|
||||
@@ -673,27 +667,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryCollectionCall) {
|
||||
flake8_comprehensions::rules::unnecessary_collection_call(
|
||||
checker,
|
||||
expr,
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
call,
|
||||
&checker.settings.flake8_comprehensions,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinTupleCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_tuple_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinListCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_list_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_list_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryLiteralWithinDictCall) {
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(
|
||||
checker, expr, func, args, keywords,
|
||||
);
|
||||
flake8_comprehensions::rules::unnecessary_literal_within_dict_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryListCall) {
|
||||
flake8_comprehensions::rules::unnecessary_list_call(checker, expr, func, args);
|
||||
@@ -976,7 +961,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnsortedDunderAll) {
|
||||
ruff::rules::sort_dunder_all_extend_call(checker, call);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::DefaultFactoryKwarg) {
|
||||
ruff::rules::default_factory_kwarg(checker, call);
|
||||
}
|
||||
@@ -1041,6 +1025,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_literal);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::MissingFStringSyntax) {
|
||||
for string_literal in value.literals() {
|
||||
ruff::rules::missing_fstring_syntax(
|
||||
&mut checker.diagnostics,
|
||||
string_literal,
|
||||
checker.locator,
|
||||
&checker.semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
@@ -1312,12 +1306,22 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::math_constant(checker, number_literal);
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => {
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_part in value {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::MissingFStringSyntax) {
|
||||
for string_literal in value.as_slice() {
|
||||
ruff::rules::missing_fstring_syntax(
|
||||
&mut checker.diagnostics,
|
||||
string_literal,
|
||||
checker.locator,
|
||||
&checker.semantic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::IfExp(
|
||||
if_exp @ ast::ExprIfExp {
|
||||
|
||||
@@ -513,9 +513,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SingleStringSlots) {
|
||||
pylint::rules::single_string_slots(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::BadDunderMethodName) {
|
||||
pylint::rules::bad_dunder_method_name(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::MetaClassABCMeta) {
|
||||
refurb::rules::metaclass_abcmeta(checker, class_def);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ impl<'a> Checker<'a> {
|
||||
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
|
||||
|
||||
// Invert the quote character, if it's a single quote.
|
||||
match *trailing_quote {
|
||||
match trailing_quote {
|
||||
"'" => Some(Quote::Double),
|
||||
"\"" => Some(Quote::Single),
|
||||
_ => None,
|
||||
@@ -349,13 +349,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Track each top-level import, to guide import insertions.
|
||||
if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the flags prior to any further descent, so that we can restore them after visiting
|
||||
// the node.
|
||||
let flags_snapshot = self.semantic.flags;
|
||||
@@ -371,14 +364,22 @@ where
|
||||
self.handle_node_load(target);
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if alias.name.contains('.') && alias.asname.is_none() {
|
||||
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
|
||||
// "foo.bar".
|
||||
let name = alias.name.split('.').next().unwrap();
|
||||
// Given `import foo.bar`, `module` would be "foo", and `call_path` would be
|
||||
// `["foo", "bar"]`.
|
||||
let module = alias.name.split('.').next().unwrap();
|
||||
|
||||
// Mark the top-level module as "seen" by the semantic model.
|
||||
self.semantic.add_module(module);
|
||||
|
||||
if alias.asname.is_none() && alias.name.contains('.') {
|
||||
let call_path: Box<[&str]> = alias.name.split('.').collect();
|
||||
self.add_binding(
|
||||
name,
|
||||
module,
|
||||
alias.identifier(),
|
||||
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
|
||||
BindingFlags::EXTERNAL,
|
||||
@@ -413,8 +414,20 @@ where
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
if self.semantic.at_top_level() {
|
||||
self.importer.visit_import(stmt);
|
||||
}
|
||||
|
||||
let module = module.as_deref();
|
||||
let level = *level;
|
||||
|
||||
// Mark the top-level module as "seen" by the semantic model.
|
||||
if level.map_or(true, |level| level == 0) {
|
||||
if let Some(module) = module.and_then(|module| module.split('.').next()) {
|
||||
self.semantic.add_module(module);
|
||||
}
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if let Some("__future__") = module {
|
||||
let name = alias.asname.as_ref().unwrap_or(&alias.name);
|
||||
|
||||
@@ -937,6 +937,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
|
||||
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
|
||||
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
|
||||
(Ruff, "027") => (RuleGroup::Preview, rules::ruff::rules::MissingFStringSyntax),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
#[cfg(feature = "test-rules")]
|
||||
|
||||
@@ -8,6 +8,7 @@ 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::textwrap::dedent_to;
|
||||
use ruff_python_trivia::{
|
||||
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
|
||||
SimpleTokenizer,
|
||||
@@ -15,7 +16,9 @@ use ruff_python_trivia::{
|
||||
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::cst::matchers::{match_function_def, match_indented_block, match_statement};
|
||||
use crate::fix::codemods;
|
||||
use crate::fix::codemods::CodegenStylist;
|
||||
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
|
||||
|
||||
/// Return the `Fix` to use when deleting a `Stmt`.
|
||||
@@ -166,6 +169,50 @@ pub(crate) fn add_argument(
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely adjust the indentation of the indented block at [`TextRange`].
|
||||
///
|
||||
/// The [`TextRange`] is assumed to represent an entire indented block, including the leading
|
||||
/// indentation of that block. For example, to dedent the body here:
|
||||
/// ```python
|
||||
/// if True:
|
||||
/// print("Hello, world!")
|
||||
/// ```
|
||||
///
|
||||
/// The range would be the entirety of ` print("Hello, world!")`.
|
||||
pub(crate) fn adjust_indentation(
|
||||
range: TextRange,
|
||||
indentation: &str,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
) -> Result<String> {
|
||||
// If the range includes a multi-line string, use LibCST to ensure that we don't adjust the
|
||||
// whitespace _within_ the string.
|
||||
if indexer.multiline_ranges().intersects(range) || indexer.fstring_ranges().intersects(range) {
|
||||
let contents = locator.slice(range);
|
||||
|
||||
let module_text = format!("def f():{}{contents}", stylist.line_ending().as_str());
|
||||
|
||||
let mut tree = match_statement(&module_text)?;
|
||||
|
||||
let embedding = match_function_def(&mut tree)?;
|
||||
|
||||
let indented_block = match_indented_block(&mut embedding.body)?;
|
||||
indented_block.indent = Some(indentation);
|
||||
|
||||
let module_text = indented_block.codegen_stylist(stylist);
|
||||
let module_text = module_text
|
||||
.strip_prefix(stylist.line_ending().as_str())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Ok(module_text)
|
||||
} else {
|
||||
// Otherwise, we can do a simple adjustment ourselves.
|
||||
let contents = locator.slice(range);
|
||||
Ok(dedent_to(contents, indentation))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/// See: [eradicate.py](https://github.com/myint/eradicate/blob/98f199940979c94447a461d50d27862b118b282d/eradicate.py)
|
||||
use aho_corasick::AhoCorasick;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Regex, RegexSet};
|
||||
|
||||
use ruff_python_parser::parse_suite;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
static CODE_INDICATORS: Lazy<AhoCorasick> = Lazy::new(|| {
|
||||
AhoCorasick::new([
|
||||
"(", ")", "[", "]", "{", "}", ":", "=", "%", "print", "return", "break", "continue",
|
||||
"import",
|
||||
"(", ")", "[", "]", "{", "}", ":", "=", "%", "return", "break", "continue", "import",
|
||||
])
|
||||
.unwrap()
|
||||
});
|
||||
@@ -44,6 +46,14 @@ pub(crate) fn comment_contains_code(line: &str, task_tags: &[String]) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path: if the comment contains consecutive identifiers, we know it won't parse.
|
||||
let tokenizer = SimpleTokenizer::starts_at(TextSize::default(), line).skip_trivia();
|
||||
if tokenizer.tuple_windows().any(|(first, second)| {
|
||||
first.kind == SimpleTokenKind::Name && second.kind == SimpleTokenKind::Name
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore task tag comments (e.g., "# TODO(tom): Refactor").
|
||||
if line
|
||||
.split(&[' ', ':', '('])
|
||||
@@ -123,9 +133,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn comment_contains_code_with_print() {
|
||||
assert!(comment_contains_code("#print", &[]));
|
||||
assert!(comment_contains_code("#print(1)", &[]));
|
||||
|
||||
assert!(!comment_contains_code("#print", &[]));
|
||||
assert!(!comment_contains_code("#print 1", &[]));
|
||||
assert!(!comment_contains_code("#to print", &[]));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -47,6 +47,10 @@ impl Violation for SixPY3 {
|
||||
|
||||
/// YTT202
|
||||
pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::SIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_python_ast::ExprCall;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -42,17 +43,19 @@ impl Violation for BlockingOsCallInAsyncFunction {
|
||||
|
||||
/// ASYNC102
|
||||
pub(crate) fn blocking_os_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if checker.semantic().in_async_context() {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.as_ref()
|
||||
.is_some_and(is_unsafe_os_method)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingOsCallInAsyncFunction,
|
||||
call.func.range(),
|
||||
));
|
||||
if checker.semantic().seen_module(Modules::OS) {
|
||||
if checker.semantic().in_async_context() {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.as_ref()
|
||||
.is_some_and(is_unsafe_os_method)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BlockingOsCallInAsyncFunction,
|
||||
call.func.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -60,6 +60,10 @@ enum Reason {
|
||||
|
||||
/// S103
|
||||
pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::OS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -35,6 +36,10 @@ impl Violation for DjangoRawSql {
|
||||
|
||||
/// S611
|
||||
pub(crate) fn django_raw_sql(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -35,6 +36,10 @@ impl Violation for LoggingConfigInsecureListen {
|
||||
|
||||
/// S612
|
||||
pub(crate) fn logging_config_insecure_listen(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::LOGGING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
@@ -48,6 +49,10 @@ impl Violation for TarfileUnsafeMembers {
|
||||
|
||||
/// S202
|
||||
pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TARFILE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !call
|
||||
.func
|
||||
.as_attribute_expr()
|
||||
@@ -65,10 +70,6 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker.semantic().seen(&["tarfile"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(TarfileUnsafeMembers, call.func.range()));
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::checkers::ast::Checker;
|
||||
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
|
||||
///
|
||||
/// dsa.generate_private_key(key_size=512)
|
||||
/// ec.generate_private_key(curve=ec.SECT163K1)
|
||||
/// ec.generate_private_key(curve=ec.SECT163K1())
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
@@ -27,7 +27,7 @@ use crate::checkers::ast::Checker;
|
||||
/// from cryptography.hazmat.primitives.asymmetric import dsa, ec
|
||||
///
|
||||
/// dsa.generate_private_key(key_size=4096)
|
||||
/// ec.generate_private_key(curve=ec.SECP384R1)
|
||||
/// ec.generate_private_key(curve=ec.SECP384R1())
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -191,6 +191,11 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel)
|
||||
}
|
||||
// Ex) `typing.Union[bool, int]`
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
// If the typing modules were never imported, we'll never match below.
|
||||
if !semantic.seen_typing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let call_path = semantic.resolve_call_path(value);
|
||||
if call_path
|
||||
.as_ref()
|
||||
|
||||
@@ -83,7 +83,6 @@ impl Violation for MutableArgumentDefault {
|
||||
|
||||
/// B006
|
||||
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
|
||||
// Scan in reverse order to right-align zip().
|
||||
for ParameterWithDefault {
|
||||
parameter,
|
||||
default,
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_python_ast::{self as ast};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -56,6 +57,10 @@ impl Violation for ReSubPositionalArgs {
|
||||
|
||||
/// B034
|
||||
pub(crate) fn re_sub_positional_args(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::RE) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(method) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::iter;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
use libcst_native::{
|
||||
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
|
||||
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
|
||||
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
|
||||
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
|
||||
ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
|
||||
RightCurlyBrace, RightParen, RightSquareBracket, SetComp, SimpleString, SimpleWhitespace,
|
||||
TrailingWhitespace, Tuple,
|
||||
};
|
||||
use std::iter;
|
||||
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -24,77 +25,10 @@ use crate::{
|
||||
checkers::ast::Checker,
|
||||
cst::matchers::{
|
||||
match_arg, match_call, match_call_mut, match_expression, match_generator_exp, match_lambda,
|
||||
match_list_comp, match_name, match_tuple,
|
||||
match_list_comp, match_tuple,
|
||||
},
|
||||
};
|
||||
|
||||
/// (C400) Convert `list(x for x in y)` to `[x for x in y]`.
|
||||
pub(crate) fn fix_unnecessary_generator_list(
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let generator_exp = match_generator_exp(&arg.value)?;
|
||||
|
||||
tree = Expression::ListComp(Box::new(ListComp {
|
||||
elt: generator_exp.elt.clone(),
|
||||
for_in: generator_exp.for_in.clone(),
|
||||
lbracket: LeftSquareBracket {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbracket: RightSquareBracket {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: generator_exp.lpar.clone(),
|
||||
rpar: generator_exp.rpar.clone(),
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
|
||||
pub(crate) fn fix_unnecessary_generator_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let generator_exp = match_generator_exp(&arg.value)?;
|
||||
|
||||
tree = Expression::SetComp(Box::new(SetComp {
|
||||
elt: generator_exp.elt.clone(),
|
||||
for_in: generator_exp.for_in.clone(),
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: generator_exp.lpar.clone(),
|
||||
rpar: generator_exp.rpar.clone(),
|
||||
}));
|
||||
|
||||
let content = tree.codegen_stylist(stylist);
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(content, expr.range(), checker.locator(), checker.semantic()),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
|
||||
/// range(3)}`.
|
||||
pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
@@ -151,46 +85,6 @@ pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) ->
|
||||
))
|
||||
}
|
||||
|
||||
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
|
||||
pub(crate) fn fix_unnecessary_list_comprehension_set(
|
||||
expr: &Expr,
|
||||
checker: &Checker,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
// Expr(Call(ListComp)))) ->
|
||||
// Expr(SetComp)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let list_comp = match_list_comp(&arg.value)?;
|
||||
|
||||
tree = Expression::SetComp(Box::new(SetComp {
|
||||
elt: list_comp.elt.clone(),
|
||||
for_in: list_comp.for_in.clone(),
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: arg.whitespace_after_arg.clone(),
|
||||
},
|
||||
lpar: list_comp.lpar.clone(),
|
||||
rpar: list_comp.rpar.clone(),
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in
|
||||
/// range(3)}`.
|
||||
pub(crate) fn fix_unnecessary_list_comprehension_dict(
|
||||
@@ -251,95 +145,6 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
|
||||
))
|
||||
}
|
||||
|
||||
/// Drop a trailing comma from a list of tuple elements.
|
||||
fn drop_trailing_comma<'a>(
|
||||
tuple: &Tuple<'a>,
|
||||
) -> Result<(
|
||||
Vec<Element<'a>>,
|
||||
ParenthesizableWhitespace<'a>,
|
||||
ParenthesizableWhitespace<'a>,
|
||||
)> {
|
||||
let whitespace_after = tuple
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after
|
||||
.clone();
|
||||
let whitespace_before = tuple
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before
|
||||
.clone();
|
||||
|
||||
let mut elements = tuple.elements.clone();
|
||||
if elements.len() == 1 {
|
||||
if let Some(Element::Simple {
|
||||
value,
|
||||
comma: Some(..),
|
||||
..
|
||||
}) = elements.last()
|
||||
{
|
||||
if whitespace_before == ParenthesizableWhitespace::default()
|
||||
&& whitespace_after == ParenthesizableWhitespace::default()
|
||||
{
|
||||
elements[0] = Element::Simple {
|
||||
value: value.clone(),
|
||||
comma: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((elements, whitespace_after, whitespace_before))
|
||||
}
|
||||
|
||||
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
|
||||
pub(crate) fn fix_unnecessary_literal_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call(List|Tuple)))) -> Expr(Set)))
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => drop_trailing_comma(inner)?,
|
||||
Expression::List(inner) => (
|
||||
inner.elements.clone(),
|
||||
inner.lbracket.whitespace_after.clone(),
|
||||
inner.rbracket.whitespace_before.clone(),
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
|
||||
if elements.is_empty() {
|
||||
call.args = vec![];
|
||||
} else {
|
||||
tree = Expression::Set(Box::new(Set {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace { whitespace_after },
|
||||
rbrace: RightCurlyBrace { whitespace_before },
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
|
||||
pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
@@ -408,126 +213,82 @@ pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Re
|
||||
))
|
||||
}
|
||||
|
||||
/// (C408)
|
||||
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> {
|
||||
enum Collection {
|
||||
Tuple,
|
||||
List,
|
||||
Dict,
|
||||
}
|
||||
|
||||
/// (C408) Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
|
||||
pub(crate) fn fix_unnecessary_collection_call(
|
||||
expr: &ast::ExprCall,
|
||||
checker: &Checker,
|
||||
) -> Result<Edit> {
|
||||
let locator = checker.locator();
|
||||
let stylist = checker.stylist();
|
||||
|
||||
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
|
||||
// Expr(Call("dict")))) -> Expr(Dict)
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call(&tree)?;
|
||||
let name = match_name(&call.func)?;
|
||||
let collection = match name.value {
|
||||
"tuple" => Collection::Tuple,
|
||||
"list" => Collection::List,
|
||||
"dict" => Collection::Dict,
|
||||
_ => bail!("Expected 'tuple', 'list', or 'dict'"),
|
||||
};
|
||||
|
||||
// Arena allocator used to create formatted strings of sufficient lifetime,
|
||||
// below.
|
||||
let mut arena: Vec<String> = vec![];
|
||||
|
||||
match collection {
|
||||
Collection::Tuple => {
|
||||
tree = Expression::Tuple(Box::new(Tuple {
|
||||
elements: vec![],
|
||||
lpar: vec![LeftParen::default()],
|
||||
rpar: vec![RightParen::default()],
|
||||
}));
|
||||
}
|
||||
Collection::List => {
|
||||
tree = Expression::List(Box::new(List {
|
||||
elements: vec![],
|
||||
lbracket: LeftSquareBracket::default(),
|
||||
rbracket: RightSquareBracket::default(),
|
||||
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
|
||||
|
||||
// Quote each argument.
|
||||
for arg in &call.args {
|
||||
let quoted = format!(
|
||||
"{}{}{}",
|
||||
quote,
|
||||
arg.keyword
|
||||
.as_ref()
|
||||
.expect("Expected dictionary argument to be kwarg")
|
||||
.value,
|
||||
quote,
|
||||
);
|
||||
arena.push(quoted);
|
||||
}
|
||||
|
||||
let elements = call
|
||||
.args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| DictElement::Simple {
|
||||
key: Expression::SimpleString(Box::new(SimpleString {
|
||||
value: &arena[i],
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
Collection::Dict => {
|
||||
if call.args.is_empty() {
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements: vec![],
|
||||
lbrace: LeftCurlyBrace::default(),
|
||||
rbrace: RightCurlyBrace::default(),
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
} else {
|
||||
let quote = checker.f_string_quote_style().unwrap_or(stylist.quote());
|
||||
})),
|
||||
value: arg.value.clone(),
|
||||
comma: arg.comma.clone(),
|
||||
whitespace_before_colon: ParenthesizableWhitespace::default(),
|
||||
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
|
||||
" ",
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Quote each argument.
|
||||
for arg in &call.args {
|
||||
let quoted = format!(
|
||||
"{}{}{}",
|
||||
quote,
|
||||
arg.keyword
|
||||
.as_ref()
|
||||
.expect("Expected dictionary argument to be kwarg")
|
||||
.value,
|
||||
quote,
|
||||
);
|
||||
arena.push(quoted);
|
||||
}
|
||||
|
||||
let elements = call
|
||||
.args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| DictElement::Simple {
|
||||
key: Expression::SimpleString(Box::new(SimpleString {
|
||||
value: &arena[i],
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
})),
|
||||
value: arg.value.clone(),
|
||||
comma: arg.comma.clone(),
|
||||
whitespace_before_colon: ParenthesizableWhitespace::default(),
|
||||
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
|
||||
SimpleWhitespace(" "),
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: call
|
||||
.args
|
||||
.last()
|
||||
.expect("Arguments should be non-empty")
|
||||
.whitespace_after_arg
|
||||
.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
tree = Expression::Dict(Box::new(Dict {
|
||||
elements,
|
||||
lbrace: LeftCurlyBrace {
|
||||
whitespace_after: call.whitespace_before_args.clone(),
|
||||
},
|
||||
rbrace: RightCurlyBrace {
|
||||
whitespace_before: call
|
||||
.args
|
||||
.last()
|
||||
.expect("Arguments should be non-empty")
|
||||
.whitespace_after_arg
|
||||
.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
if matches!(collection, Collection::Dict) {
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
)
|
||||
} else {
|
||||
tree.codegen_stylist(stylist)
|
||||
},
|
||||
pad_expression(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
checker.locator(),
|
||||
checker.semantic(),
|
||||
),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
@@ -544,7 +305,7 @@ pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) ->
|
||||
/// However, this is a syntax error under the f-string grammar. As such,
|
||||
/// this method will pad the start and end of an expression as needed to
|
||||
/// avoid producing invalid syntax.
|
||||
fn pad_expression(
|
||||
pub(crate) fn pad_expression(
|
||||
content: String,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
@@ -575,106 +336,46 @@ fn pad_expression(
|
||||
}
|
||||
}
|
||||
|
||||
/// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)`
|
||||
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
|
||||
expr: &Expr,
|
||||
/// Like [`pad_expression`], but only pads the start of the expression.
|
||||
pub(crate) fn pad_start(
|
||||
content: &str,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => (
|
||||
&inner.elements,
|
||||
&inner
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after,
|
||||
&inner
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before,
|
||||
),
|
||||
Expression::List(inner) => (
|
||||
&inner.elements,
|
||||
&inner.lbracket.whitespace_after,
|
||||
&inner.rbracket.whitespace_before,
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
semantic: &SemanticModel,
|
||||
) -> String {
|
||||
if !semantic.in_f_string() {
|
||||
return content.into();
|
||||
}
|
||||
|
||||
tree = Expression::Tuple(Box::new(Tuple {
|
||||
elements: elements.clone(),
|
||||
lpar: vec![LeftParen {
|
||||
whitespace_after: whitespace_after.clone(),
|
||||
}],
|
||||
rpar: vec![RightParen {
|
||||
whitespace_before: whitespace_before.clone(),
|
||||
}],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let prefix = locator.up_to(range.start());
|
||||
if matches!(prefix.chars().next_back(), Some('{')) {
|
||||
format!(" {content}")
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// (C410) Convert `list([1, 2])` to `[1, 2]`
|
||||
pub(crate) fn fix_unnecessary_literal_within_list_call(
|
||||
expr: &Expr,
|
||||
/// Like [`pad_expression`], but only pads the end of the expression.
|
||||
pub(crate) fn pad_end(
|
||||
content: &str,
|
||||
range: TextRange,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
let (elements, whitespace_after, whitespace_before) = match &arg.value {
|
||||
Expression::Tuple(inner) => (
|
||||
&inner.elements,
|
||||
&inner
|
||||
.lpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_after,
|
||||
&inner
|
||||
.rpar
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("Expected at least one set of parentheses"))?
|
||||
.whitespace_before,
|
||||
),
|
||||
Expression::List(inner) => (
|
||||
&inner.elements,
|
||||
&inner.lbracket.whitespace_after,
|
||||
&inner.rbracket.whitespace_before,
|
||||
),
|
||||
_ => {
|
||||
bail!("Expected Expression::Tuple | Expression::List");
|
||||
}
|
||||
};
|
||||
semantic: &SemanticModel,
|
||||
) -> String {
|
||||
if !semantic.in_f_string() {
|
||||
return content.into();
|
||||
}
|
||||
|
||||
tree = Expression::List(Box::new(List {
|
||||
elements: elements.clone(),
|
||||
lbracket: LeftSquareBracket {
|
||||
whitespace_after: whitespace_after.clone(),
|
||||
},
|
||||
rbracket: RightSquareBracket {
|
||||
whitespace_before: whitespace_before.clone(),
|
||||
},
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
// If the expression is immediately preceded by an opening brace, then
|
||||
// we need to add a space before the expression.
|
||||
let suffix = locator.after(range.end());
|
||||
if matches!(suffix.chars().next(), Some('}')) {
|
||||
format!("{content} ")
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`.
|
||||
@@ -1091,25 +792,6 @@ pub(crate) fn fix_unnecessary_map(
|
||||
Ok(Edit::range_replacement(content, expr.range()))
|
||||
}
|
||||
|
||||
/// (C418) Convert `dict({"a": 1})` to `{"a": 1}`
|
||||
pub(crate) fn fix_unnecessary_literal_within_dict_call(
|
||||
expr: &Expr,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(expr);
|
||||
let mut tree = match_expression(module_text)?;
|
||||
let call = match_call_mut(&mut tree)?;
|
||||
let arg = match_arg(call)?;
|
||||
|
||||
tree = arg.value.clone();
|
||||
|
||||
Ok(Edit::range_replacement(
|
||||
tree.codegen_stylist(stylist),
|
||||
expr.range(),
|
||||
))
|
||||
}
|
||||
|
||||
/// (C419) Convert `[i for i in a]` into `i for i in a`
|
||||
pub(crate) fn fix_unnecessary_comprehension_any_all(
|
||||
expr: &Expr,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
|
||||
/// ## What it does
|
||||
@@ -36,6 +36,9 @@ use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
|
||||
/// when rewriting the call. In most cases, though, comments will be preserved.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments`
|
||||
#[violation]
|
||||
pub struct UnnecessaryCollectionCall {
|
||||
obj_type: String,
|
||||
@@ -56,42 +59,88 @@ impl AlwaysFixableViolation for UnnecessaryCollectionCall {
|
||||
/// C408
|
||||
pub(crate) fn unnecessary_collection_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
call: &ast::ExprCall,
|
||||
settings: &Settings,
|
||||
) {
|
||||
if !args.is_empty() {
|
||||
if !call.arguments.args.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(func) = func.as_name_expr() else {
|
||||
let Some(func) = call.func.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
match func.id.as_str() {
|
||||
let collection = match func.id.as_str() {
|
||||
"dict"
|
||||
if keywords.is_empty()
|
||||
if call.arguments.keywords.is_empty()
|
||||
|| (!settings.allow_dict_calls_with_keyword_arguments
|
||||
&& keywords.iter().all(|kw| kw.arg.is_some())) =>
|
||||
&& call.arguments.keywords.iter().all(|kw| kw.arg.is_some())) =>
|
||||
{
|
||||
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
|
||||
Collection::Dict
|
||||
}
|
||||
"list" | "tuple" => {
|
||||
// `list()` or `tuple()`
|
||||
"list" if call.arguments.keywords.is_empty() => {
|
||||
// `list()
|
||||
Collection::List
|
||||
}
|
||||
"tuple" if call.arguments.keywords.is_empty() => {
|
||||
// `tuple()`
|
||||
Collection::Tuple
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
if !checker.semantic().is_builtin(func.id.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryCollectionCall {
|
||||
obj_type: func.id.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_collection_call(expr, checker).map(Fix::unsafe_edit)
|
||||
});
|
||||
|
||||
// Convert `dict()` to `{}`.
|
||||
if call.arguments.keywords.is_empty() {
|
||||
diagnostic.set_fix({
|
||||
// Replace from the start of the call to the start of the argument.
|
||||
let call_start = Edit::replacement(
|
||||
match collection {
|
||||
Collection::Dict => {
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic())
|
||||
}
|
||||
Collection::List => "[".to_string(),
|
||||
Collection::Tuple => "(".to_string(),
|
||||
},
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner list or tuple to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
match collection {
|
||||
Collection::Dict => {
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic())
|
||||
}
|
||||
Collection::List => "]".to_string(),
|
||||
Collection::Tuple => ")".to_string(),
|
||||
},
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
} else {
|
||||
// Convert `dict(a=1, b=2)` to `{"a": 1, "b": 2}`.
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_collection_call(call, checker).map(Fix::unsafe_edit)
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
enum Collection {
|
||||
Tuple,
|
||||
List,
|
||||
Dict,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
@@ -47,27 +44,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorList {
|
||||
}
|
||||
|
||||
/// C400 (`list(generator)`)
|
||||
pub(crate) fn unnecessary_generator_list(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("list", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"list",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("list") {
|
||||
return;
|
||||
}
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_list(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
if argument.is_generator_exp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, call.range());
|
||||
|
||||
// Convert `list(x for x in y)` to `[x for x in y]`.
|
||||
diagnostic.set_fix({
|
||||
// Replace `list(` with `[`.
|
||||
let call_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let call_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -47,26 +45,40 @@ impl AlwaysFixableViolation for UnnecessaryGeneratorSet {
|
||||
}
|
||||
|
||||
/// C401 (`set(generator)`)
|
||||
pub(crate) fn unnecessary_generator_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
return;
|
||||
}
|
||||
if let Expr::GeneratorExp(_) = argument {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_generator_set(expr, checker).map(Fix::unsafe_edit)
|
||||
if argument.is_generator_exp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, call.range());
|
||||
|
||||
// Convert `set(x for x in y)` to `{x for x in y}`.
|
||||
diagnostic.set_fix({
|
||||
// Replace `set(` with `}`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -45,25 +43,43 @@ impl AlwaysFixableViolation for UnnecessaryListComprehensionSet {
|
||||
}
|
||||
|
||||
/// C403 (`set([...])`)
|
||||
pub(crate) fn unnecessary_list_comprehension_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_list_comprehension_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
return;
|
||||
}
|
||||
if argument.is_list_comp_expr() {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_list_comprehension_set(expr, checker).map(Fix::unsafe_edit)
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, call.range());
|
||||
diagnostic.set_fix({
|
||||
// Replace `set(` with `{`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
call.arguments.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
// Delete the open bracket (`[`).
|
||||
let argument_start =
|
||||
Edit::deletion(argument.start(), argument.start() + TextSize::from(1));
|
||||
|
||||
// Delete the close bracket (`]`).
|
||||
let argument_end = Edit::deletion(argument.end() - TextSize::from(1), argument.end());
|
||||
|
||||
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
|
||||
use super::helpers;
|
||||
|
||||
@@ -53,16 +51,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralSet {
|
||||
}
|
||||
|
||||
/// C405 (`set([1, 2])`)
|
||||
pub(crate) fn unnecessary_literal_set(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
let Some(argument) =
|
||||
helpers::exactly_one_argument_with_matching_function("set", func, args, keywords)
|
||||
else {
|
||||
pub(crate) fn unnecessary_literal_set(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
let Some(argument) = helpers::exactly_one_argument_with_matching_function(
|
||||
"set",
|
||||
&call.func,
|
||||
&call.arguments.args,
|
||||
&call.arguments.keywords,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("set") {
|
||||
@@ -73,13 +68,69 @@ pub(crate) fn unnecessary_literal_set(
|
||||
Expr::Tuple(_) => "tuple",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralSet {
|
||||
obj_type: kind.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic
|
||||
.try_set_fix(|| fixes::fix_unnecessary_literal_set(expr, checker).map(Fix::unsafe_edit));
|
||||
|
||||
// Convert `set((1, 2))` to `{1, 2}`.
|
||||
diagnostic.set_fix({
|
||||
let elts = match &argument {
|
||||
Expr::List(ast::ExprList { elts, .. }) => elts,
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match elts.as_slice() {
|
||||
// If the list or tuple is empty, replace the entire call with `set()`.
|
||||
[] => Fix::unsafe_edit(Edit::range_replacement("set()".to_string(), call.range())),
|
||||
// If it's a single-element tuple (with no whitespace around it), remove the trailing
|
||||
// comma.
|
||||
[elt]
|
||||
if argument.is_tuple_expr()
|
||||
// The element must start right after the `(`.
|
||||
&& elt.start() == argument.start() + TextSize::new(1)
|
||||
// The element must be followed by exactly one comma and a closing `)`.
|
||||
&& elt.end() + TextSize::new(2) == argument.end() =>
|
||||
{
|
||||
// Replace from the start of the call to the start of the inner element.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
elt.start(),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner element to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
elt.end(),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
_ => {
|
||||
// Replace from the start of the call to the start of the inner list or tuple with `{`.
|
||||
let call_start = Edit::replacement(
|
||||
pad_start("{", call.range(), checker.locator(), checker.semantic()),
|
||||
call.start(),
|
||||
argument.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner list or tuple to the end of the call with `}`.
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
argument.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -68,17 +66,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinDictCall {
|
||||
}
|
||||
|
||||
/// C418
|
||||
pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
if !keywords.is_empty() {
|
||||
pub(crate) fn unnecessary_literal_within_dict_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(argument) = helpers::first_argument_with_matching_function("dict", func, args) else {
|
||||
let Some(argument) =
|
||||
helpers::first_argument_with_matching_function("dict", &call.func, &call.arguments.args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("dict") {
|
||||
@@ -89,15 +83,24 @@ pub(crate) fn unnecessary_literal_within_dict_call(
|
||||
Expr::Dict(_) => DictKind::Literal,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralWithinDictCall {
|
||||
kind: argument_kind,
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_dict_call(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
|
||||
// Convert `dict({"a": 1})` to `{"a": 1}`
|
||||
diagnostic.set_fix({
|
||||
// Delete from the start of the call to the start of the argument.
|
||||
let call_start = Edit::deletion(call.start(), argument.start());
|
||||
|
||||
// Delete from the end of the argument to the end of the call.
|
||||
let call_end = Edit::deletion(argument.end(), call.end());
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
@@ -70,17 +68,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinListCall {
|
||||
}
|
||||
|
||||
/// C410
|
||||
pub(crate) fn unnecessary_literal_within_list_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
if !keywords.is_empty() {
|
||||
pub(crate) fn unnecessary_literal_within_list_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(argument) = helpers::first_argument_with_matching_function("list", func, args) else {
|
||||
let Some(argument) =
|
||||
helpers::first_argument_with_matching_function("list", &call.func, &call.arguments.args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("list") {
|
||||
@@ -91,15 +85,43 @@ pub(crate) fn unnecessary_literal_within_list_call(
|
||||
Expr::List(_) => "list",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralWithinListCall {
|
||||
literal: argument_kind.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_list_call(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
|
||||
// Convert `list([1, 2])` to `[1, 2]`
|
||||
diagnostic.set_fix({
|
||||
// Delete from the start of the call to the start of the argument.
|
||||
let call_start = Edit::deletion(call.start(), argument.start());
|
||||
|
||||
// Delete from the end of the argument to the end of the call.
|
||||
let call_end = Edit::deletion(argument.end(), call.end());
|
||||
|
||||
// If this is a tuple, we also need to convert the inner argument to a list.
|
||||
if argument.is_tuple_expr() {
|
||||
// Replace `(` with `[`.
|
||||
let argument_start = Edit::replacement(
|
||||
"[".to_string(),
|
||||
argument.start(),
|
||||
argument.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let argument_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
argument.end() - TextSize::from(1),
|
||||
argument.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [argument_start, argument_end, call_end])
|
||||
} else {
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
}
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use ruff_python_ast::{Expr, Keyword};
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
|
||||
use super::helpers;
|
||||
|
||||
/// ## What it does
|
||||
@@ -48,13 +45,11 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinTupleCall {
|
||||
let UnnecessaryLiteralWithinTupleCall { literal } = self;
|
||||
if literal == "list" {
|
||||
format!(
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` \
|
||||
literal)"
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` literal)"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (remove the outer call to \
|
||||
`tuple()`)"
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (remove the outer call to `tuple()`)"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -72,17 +67,13 @@ impl AlwaysFixableViolation for UnnecessaryLiteralWithinTupleCall {
|
||||
}
|
||||
|
||||
/// C409
|
||||
pub(crate) fn unnecessary_literal_within_tuple_call(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) {
|
||||
if !keywords.is_empty() {
|
||||
pub(crate) fn unnecessary_literal_within_tuple_call(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !call.arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(argument) = helpers::first_argument_with_matching_function("tuple", func, args) else {
|
||||
let Some(argument) =
|
||||
helpers::first_argument_with_matching_function("tuple", &call.func, &call.arguments.args)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !checker.semantic().is_builtin("tuple") {
|
||||
@@ -93,15 +84,32 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
|
||||
Expr::List(_) => "list",
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnnecessaryLiteralWithinTupleCall {
|
||||
literal: argument_kind.to_string(),
|
||||
},
|
||||
expr.range(),
|
||||
call.range(),
|
||||
);
|
||||
diagnostic.try_set_fix(|| {
|
||||
fixes::fix_unnecessary_literal_within_tuple_call(expr, checker.locator(), checker.stylist())
|
||||
.map(Fix::unsafe_edit)
|
||||
|
||||
// Convert `tuple([1, 2])` to `tuple(1, 2)`
|
||||
diagnostic.set_fix({
|
||||
// Replace from the start of the call to the start of the inner list or tuple with `(`.
|
||||
let call_start = Edit::replacement(
|
||||
"(".to_string(),
|
||||
call.start(),
|
||||
argument.start() + TextSize::from(1),
|
||||
);
|
||||
|
||||
// Replace from the end of the inner list or tuple to the end of the call with `)`.
|
||||
let call_end = Edit::replacement(
|
||||
")".to_string(),
|
||||
argument.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
|
||||
Fix::unsafe_edits(call_start, [call_end])
|
||||
});
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ C403.py:14:9: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comp
|
||||
14 |-s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
14 |+s = f"{ {x for x in 'ab'} | set([x for x in 'ab']) }"
|
||||
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
16 16 |
|
||||
17 17 | s = set( # comment
|
||||
|
||||
C403.py:14:34: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
||||
|
|
||||
@@ -138,12 +140,16 @@ C403.py:14:34: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` com
|
||||
14 |-s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
14 |+s = f"{ set([x for x in 'ab']) | {x for x in 'ab'} }"
|
||||
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
16 16 |
|
||||
17 17 | s = set( # comment
|
||||
|
||||
C403.py:15:8: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
||||
|
|
||||
14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ C403
|
||||
16 |
|
||||
17 | s = set( # comment
|
||||
|
|
||||
= help: Rewrite as a `set` comprehension
|
||||
|
||||
@@ -153,12 +159,17 @@ C403.py:15:8: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comp
|
||||
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
15 |-s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
15 |+s = f"{ {x for x in 'ab'} | set([x for x in 'ab'])}"
|
||||
16 16 |
|
||||
17 17 | s = set( # comment
|
||||
18 18 | [x for x in range(3)]
|
||||
|
||||
C403.py:15:33: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
||||
|
|
||||
14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ C403
|
||||
16 |
|
||||
17 | s = set( # comment
|
||||
|
|
||||
= help: Rewrite as a `set` comprehension
|
||||
|
||||
@@ -168,5 +179,58 @@ C403.py:15:33: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` com
|
||||
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
15 |-s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
15 |+s = f"{set([x for x in 'ab']) | {x for x in 'ab'} }"
|
||||
16 16 |
|
||||
17 17 | s = set( # comment
|
||||
18 18 | [x for x in range(3)]
|
||||
|
||||
C403.py:17:5: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
||||
|
|
||||
15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
16 |
|
||||
17 | s = set( # comment
|
||||
| _____^
|
||||
18 | | [x for x in range(3)]
|
||||
19 | | )
|
||||
| |_^ C403
|
||||
20 |
|
||||
21 | s = set([ # comment
|
||||
|
|
||||
= help: Rewrite as a `set` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
14 14 | s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
|
||||
15 15 | s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"
|
||||
16 16 |
|
||||
17 |-s = set( # comment
|
||||
18 |- [x for x in range(3)]
|
||||
19 |-)
|
||||
17 |+s = { # comment
|
||||
18 |+ x for x in range(3)
|
||||
19 |+}
|
||||
20 20 |
|
||||
21 21 | s = set([ # comment
|
||||
22 22 | x for x in range(3)
|
||||
|
||||
C403.py:21:5: C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
|
||||
|
|
||||
19 | )
|
||||
20 |
|
||||
21 | s = set([ # comment
|
||||
| _____^
|
||||
22 | | x for x in range(3)
|
||||
23 | | ])
|
||||
| |__^ C403
|
||||
|
|
||||
= help: Rewrite as a `set` comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
18 18 | [x for x in range(3)]
|
||||
19 19 | )
|
||||
20 20 |
|
||||
21 |-s = set([ # comment
|
||||
21 |+s = { # comment
|
||||
22 22 | x for x in range(3)
|
||||
23 |-])
|
||||
23 |+}
|
||||
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ C408.py:20:5: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
20 |+f"{ {'x': 'y'} | dict(y='z') }"
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
|
||||
C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
@@ -236,6 +237,7 @@ C408.py:20:19: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
20 |+f"{ dict(x='y') | {'y': 'z'} }"
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
|
||||
C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
@@ -254,6 +256,8 @@ C408.py:21:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 |-f"a {dict(x='y') | dict(y='z')} b"
|
||||
21 |+f"a { {'x': 'y'} | dict(y='z')} b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
24 24 | dict(
|
||||
|
||||
C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
@@ -272,6 +276,8 @@ C408.py:21:20: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 |-f"a {dict(x='y') | dict(y='z')} b"
|
||||
21 |+f"a {dict(x='y') | {'y': 'z'} } b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
24 24 | dict(
|
||||
|
||||
C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
@@ -279,6 +285,8 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
| ^^^^^^^^^^^ C408
|
||||
23 |
|
||||
24 | dict(
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
@@ -288,6 +296,9 @@ C408.py:22:7: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 |-f"a { dict(x='y') | dict(y='z') } b"
|
||||
22 |+f"a { {'x': 'y'} | dict(y='z') } b"
|
||||
23 23 |
|
||||
24 24 | dict(
|
||||
25 25 | # comment
|
||||
|
||||
C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
@@ -295,6 +306,8 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
| ^^^^^^^^^^^ C408
|
||||
23 |
|
||||
24 | dict(
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
@@ -304,5 +317,52 @@ C408.py:22:21: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 |-f"a { dict(x='y') | dict(y='z') } b"
|
||||
22 |+f"a { dict(x='y') | {'y': 'z'} } b"
|
||||
23 23 |
|
||||
24 24 | dict(
|
||||
25 25 | # comment
|
||||
|
||||
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 |
|
||||
24 | / dict(
|
||||
25 | | # comment
|
||||
26 | | )
|
||||
| |_^ C408
|
||||
27 |
|
||||
28 | tuple( # comment
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
24 |-dict(
|
||||
24 |+{
|
||||
25 25 | # comment
|
||||
26 |-)
|
||||
26 |+}
|
||||
27 27 |
|
||||
28 28 | tuple( # comment
|
||||
29 29 | )
|
||||
|
||||
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
||||
|
|
||||
26 | )
|
||||
27 |
|
||||
28 | / tuple( # comment
|
||||
29 | | )
|
||||
| |_^ C408
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
25 25 | # comment
|
||||
26 26 | )
|
||||
27 27 |
|
||||
28 |-tuple( # comment
|
||||
28 |+( # comment
|
||||
29 29 | )
|
||||
|
||||
|
||||
|
||||
@@ -96,4 +96,48 @@ C408.py:17:6: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
19 19 | f"{dict(x='y') | dict(y='z')}"
|
||||
20 20 | f"{ dict(x='y') | dict(y='z') }"
|
||||
|
||||
C408.py:24:1: C408 [*] Unnecessary `dict` call (rewrite as a literal)
|
||||
|
|
||||
22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 |
|
||||
24 | / dict(
|
||||
25 | | # comment
|
||||
26 | | )
|
||||
| |_^ C408
|
||||
27 |
|
||||
28 | tuple( # comment
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
21 21 | f"a {dict(x='y') | dict(y='z')} b"
|
||||
22 22 | f"a { dict(x='y') | dict(y='z') } b"
|
||||
23 23 |
|
||||
24 |-dict(
|
||||
24 |+{
|
||||
25 25 | # comment
|
||||
26 |-)
|
||||
26 |+}
|
||||
27 27 |
|
||||
28 28 | tuple( # comment
|
||||
29 29 | )
|
||||
|
||||
C408.py:28:1: C408 [*] Unnecessary `tuple` call (rewrite as a literal)
|
||||
|
|
||||
26 | )
|
||||
27 |
|
||||
28 | / tuple( # comment
|
||||
29 | | )
|
||||
| |_^ C408
|
||||
|
|
||||
= help: Rewrite as a literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
25 25 | # comment
|
||||
26 26 | )
|
||||
27 27 |
|
||||
28 |-tuple( # comment
|
||||
28 |+( # comment
|
||||
29 29 | )
|
||||
|
||||
|
||||
|
||||
@@ -93,16 +93,67 @@ C409.py:8:6: C409 [*] Unnecessary `tuple` literal passed to `tuple()` (remove th
|
||||
9 | | (1, 2)
|
||||
10 | | )
|
||||
| |_^ C409
|
||||
11 |
|
||||
12 | tuple( # comment
|
||||
|
|
||||
= help: Remove outer `tuple` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
5 5 | 1,
|
||||
6 6 | 2
|
||||
7 7 | ])
|
||||
8 |-t5 = tuple(
|
||||
9 |- (1, 2)
|
||||
10 |-)
|
||||
8 |+t5 = (1, 2)
|
||||
5 5 | 1,
|
||||
6 6 | 2
|
||||
7 7 | ])
|
||||
8 |-t5 = tuple(
|
||||
9 |- (1, 2)
|
||||
10 |-)
|
||||
8 |+t5 = (1, 2)
|
||||
11 9 |
|
||||
12 10 | tuple( # comment
|
||||
13 11 | [1, 2]
|
||||
|
||||
C409.py:12:1: C409 [*] Unnecessary `list` literal passed to `tuple()` (rewrite as a `tuple` literal)
|
||||
|
|
||||
10 | )
|
||||
11 |
|
||||
12 | / tuple( # comment
|
||||
13 | | [1, 2]
|
||||
14 | | )
|
||||
| |_^ C409
|
||||
15 |
|
||||
16 | tuple([ # comment
|
||||
|
|
||||
= help: Rewrite as a `tuple` literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
9 9 | (1, 2)
|
||||
10 10 | )
|
||||
11 11 |
|
||||
12 |-tuple( # comment
|
||||
13 |- [1, 2]
|
||||
14 |-)
|
||||
12 |+(1, 2)
|
||||
15 13 |
|
||||
16 14 | tuple([ # comment
|
||||
17 15 | 1, 2
|
||||
|
||||
C409.py:16:1: C409 [*] Unnecessary `list` literal passed to `tuple()` (rewrite as a `tuple` literal)
|
||||
|
|
||||
14 | )
|
||||
15 |
|
||||
16 | / tuple([ # comment
|
||||
17 | | 1, 2
|
||||
18 | | ])
|
||||
| |__^ C409
|
||||
|
|
||||
= help: Rewrite as a `tuple` literal
|
||||
|
||||
ℹ Unsafe fix
|
||||
13 13 | [1, 2]
|
||||
14 14 | )
|
||||
15 15 |
|
||||
16 |-tuple([ # comment
|
||||
16 |+( # comment
|
||||
17 17 | 1, 2
|
||||
18 |-])
|
||||
18 |+)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ C410.py:2:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as
|
||||
2 |+l2 = [1, 2]
|
||||
3 3 | l3 = list([])
|
||||
4 4 | l4 = list(())
|
||||
5 5 |
|
||||
|
||||
C410.py:3:6: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
||||
|
|
||||
@@ -50,6 +51,8 @@ C410.py:3:6: C410 [*] Unnecessary `list` literal passed to `list()` (remove the
|
||||
3 |-l3 = list([])
|
||||
3 |+l3 = []
|
||||
4 4 | l4 = list(())
|
||||
5 5 |
|
||||
6 6 |
|
||||
|
||||
C410.py:4:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as a `list` literal)
|
||||
|
|
||||
@@ -66,5 +69,52 @@ C410.py:4:6: C410 [*] Unnecessary `tuple` literal passed to `list()` (rewrite as
|
||||
3 3 | l3 = list([])
|
||||
4 |-l4 = list(())
|
||||
4 |+l4 = []
|
||||
5 5 |
|
||||
6 6 |
|
||||
7 7 | list( # comment
|
||||
|
||||
C410.py:7:1: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
||||
|
|
||||
7 | / list( # comment
|
||||
8 | | [1, 2]
|
||||
9 | | )
|
||||
| |_^ C410
|
||||
10 |
|
||||
11 | list([ # comment
|
||||
|
|
||||
= help: Remove outer `list` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | l4 = list(())
|
||||
5 5 |
|
||||
6 6 |
|
||||
7 |-list( # comment
|
||||
8 |- [1, 2]
|
||||
9 |-)
|
||||
7 |+[1, 2]
|
||||
10 8 |
|
||||
11 9 | list([ # comment
|
||||
12 10 | 1, 2
|
||||
|
||||
C410.py:11:1: C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
|
||||
|
|
||||
9 | )
|
||||
10 |
|
||||
11 | / list([ # comment
|
||||
12 | | 1, 2
|
||||
13 | | ])
|
||||
| |__^ C410
|
||||
|
|
||||
= help: Remove outer `list` call
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | [1, 2]
|
||||
9 9 | )
|
||||
10 10 |
|
||||
11 |-list([ # comment
|
||||
11 |+[ # comment
|
||||
12 12 | 1, 2
|
||||
13 |-])
|
||||
13 |+]
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -57,6 +58,10 @@ impl Violation for CallDateFromtimestamp {
|
||||
}
|
||||
|
||||
pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -56,6 +57,10 @@ impl Violation for CallDateToday {
|
||||
}
|
||||
|
||||
pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -60,6 +61,10 @@ impl Violation for CallDatetimeFromtimestamp {
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -56,6 +57,10 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -64,6 +65,10 @@ impl Violation for CallDatetimeStrptimeWithoutZone {
|
||||
|
||||
/// DTZ007
|
||||
pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -55,6 +56,10 @@ impl Violation for CallDatetimeToday {
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_today(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -63,6 +64,10 @@ pub(crate) fn call_datetime_utcfromtimestamp(
|
||||
func: &Expr,
|
||||
location: TextRange,
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -57,7 +58,12 @@ impl Violation for CallDatetimeUtcnow {
|
||||
}
|
||||
}
|
||||
|
||||
/// DTZ003
|
||||
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -52,6 +53,10 @@ impl Violation for CallDatetimeWithoutTzinfo {
|
||||
}
|
||||
|
||||
pub(crate) fn call_datetime_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DATETIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,6 +49,10 @@ impl Violation for DjangoAllWithModelForm {
|
||||
|
||||
/// DJ007
|
||||
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -46,6 +47,10 @@ impl Violation for DjangoExcludeWithModelForm {
|
||||
|
||||
/// DJ006
|
||||
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_model_form(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -45,6 +45,10 @@ impl Violation for DjangoLocalsInRenderFunction {
|
||||
|
||||
/// DJ003
|
||||
pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(&call.func)
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_true;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::{analyze, SemanticModel};
|
||||
use ruff_python_semantic::{analyze, Modules, SemanticModel};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -52,6 +52,10 @@ impl Violation for DjangoModelWithoutDunderStr {
|
||||
|
||||
/// DJ008
|
||||
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !is_non_abstract_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast::Decorator;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -51,6 +52,10 @@ impl Violation for DjangoNonLeadingReceiverDecorator {
|
||||
|
||||
/// DJ013
|
||||
pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut seen_receiver = false;
|
||||
for (i, decorator) in decorator_list.iter().enumerate() {
|
||||
let is_receiver = decorator.expression.as_call_expr().is_some_and(|call| {
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_const_true;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -55,6 +55,10 @@ impl Violation for DjangoNullableModelStringField {
|
||||
|
||||
/// DJ001
|
||||
pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
for statement in body {
|
||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
|
||||
continue;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::{Modules, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -83,6 +83,10 @@ pub(crate) fn unordered_body_content_in_model(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::DJANGO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !helpers::is_model(class_def, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
@@ -101,28 +101,37 @@ pub(crate) fn implicit(
|
||||
for ((a_tok, a_range), (b_tok, b_range)) in tokens
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|(tok, _)| {
|
||||
!tok.is_comment()
|
||||
&& (settings.flake8_implicit_str_concat.allow_multiline
|
||||
|| !tok.is_non_logical_newline())
|
||||
.filter_map(|(tok, range)| {
|
||||
// Ignore comments.
|
||||
if tok.is_comment() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ignore non-localized newlines.
|
||||
if !settings.flake8_implicit_str_concat.allow_multiline && tok.is_non_logical_newline()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((TokenKind::from(tok), *range))
|
||||
})
|
||||
.tuple_windows()
|
||||
{
|
||||
let (a_range, b_range) = match (a_tok, b_tok) {
|
||||
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
|
||||
(Tok::String { .. }, Tok::FStringStart) => {
|
||||
(TokenKind::String, TokenKind::String) => (a_range, b_range),
|
||||
(TokenKind::String, TokenKind::FStringStart) => {
|
||||
match indexer.fstring_ranges().innermost(b_range.start()) {
|
||||
Some(b_range) => (*a_range, b_range),
|
||||
Some(b_range) => (a_range, b_range),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
(Tok::FStringEnd, Tok::String { .. }) => {
|
||||
(TokenKind::FStringEnd, TokenKind::String { .. }) => {
|
||||
match indexer.fstring_ranges().innermost(a_range.start()) {
|
||||
Some(a_range) => (a_range, *b_range),
|
||||
Some(a_range) => (a_range, b_range),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
(Tok::FStringEnd, Tok::FStringStart) => {
|
||||
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
|
||||
match (
|
||||
indexer.fstring_ranges().innermost(a_range.start()),
|
||||
indexer.fstring_ranges().innermost(b_range.start()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -54,6 +55,10 @@ impl Violation for DirectLoggerInstantiation {
|
||||
|
||||
/// LOG001
|
||||
pub(crate) fn direct_logger_instantiation(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::LOGGING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -57,6 +58,10 @@ impl Violation for InvalidGetLoggerArgument {
|
||||
|
||||
/// LOG002
|
||||
pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::LOGGING) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(Expr::Name(expr @ ast::ExprName { id, .. })) = call.arguments.find_argument("name", 0)
|
||||
else {
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,6 +49,10 @@ impl Violation for UndocumentedWarn {
|
||||
|
||||
/// LOG009
|
||||
pub(crate) fn undocumented_warn(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::LOGGING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::AlwaysFixableViolation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::whitespace::trailing_comment_start_offset;
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_python_semantic::{ScopeKind, SemanticModel};
|
||||
@@ -132,7 +133,7 @@ fn in_protocol_or_abstract_method(semantic: &SemanticModel) -> bool {
|
||||
ScopeKind::Class(class_def) => class_def
|
||||
.bases()
|
||||
.iter()
|
||||
.any(|base| semantic.match_typing_expr(base, "Protocol")),
|
||||
.any(|base| semantic.match_typing_expr(map_subscript(base), "Protocol")),
|
||||
ScopeKind::Function(function_def) => {
|
||||
ruff_python_semantic::analyze::visibility::is_abstract(
|
||||
&function_def.decorator_list,
|
||||
|
||||
@@ -671,5 +671,8 @@ PIE790.py:209:9: PIE790 [*] Unnecessary `...` literal
|
||||
207 207 | def stub(self) -> str:
|
||||
208 208 | """Docstring"""
|
||||
209 |- ...
|
||||
210 209 |
|
||||
211 210 |
|
||||
212 211 | class Repro(Protocol[int]):
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -50,6 +51,10 @@ impl Violation for CollectionsNamedTuple {
|
||||
|
||||
/// PYI024
|
||||
pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::COLLECTIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -61,44 +60,53 @@ impl Violation for UnprefixedTypeParam {
|
||||
|
||||
/// PYI001
|
||||
pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
|
||||
// If the typing modules were never imported, we'll never match below.
|
||||
if !checker.semantic().seen_typing() {
|
||||
return;
|
||||
}
|
||||
|
||||
let [target] = targets else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target {
|
||||
if id.starts_with('_') {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = value {
|
||||
let Some(kind) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.and_then(|call_path| {
|
||||
if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "ParamSpec")
|
||||
{
|
||||
Some(VarKind::ParamSpec)
|
||||
} else if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "TypeVar")
|
||||
{
|
||||
Some(VarKind::TypeVar)
|
||||
} else if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "TypeVarTuple")
|
||||
{
|
||||
Some(VarKind::TypeVarTuple)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range()));
|
||||
}
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(kind) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.and_then(|call_path| {
|
||||
if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "ParamSpec")
|
||||
{
|
||||
Some(VarKind::ParamSpec)
|
||||
} else if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "TypeVar")
|
||||
{
|
||||
Some(VarKind::TypeVar)
|
||||
} else if checker
|
||||
.semantic()
|
||||
.match_typing_call_path(&call_path, "TypeVarTuple")
|
||||
{
|
||||
Some(VarKind::TypeVarTuple)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range()));
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ pub(crate) fn avoidable_escaped_quote(
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
@@ -361,6 +362,7 @@ pub(crate) fn unnecessary_escaped_quote(
|
||||
Tok::FStringMiddle {
|
||||
value: string_contents,
|
||||
is_raw,
|
||||
triple_quoted: _,
|
||||
} if !is_raw => {
|
||||
let Some(context) = fstrings.last_mut() else {
|
||||
continue;
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
use anyhow::Result;
|
||||
use std::ops::Add;
|
||||
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
use ruff_python_ast::helpers::{is_const_false, is_const_true};
|
||||
use ruff_python_ast::stmt_if::elif_else_range;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_trivia::{is_python_whitespace, SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits;
|
||||
use crate::fix::edits::adjust_indentation;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::flake8_return::helpers::end_of_last_statement;
|
||||
use crate::rules::pyupgrade::fixes::adjust_indentation;
|
||||
|
||||
use super::super::branch::Branch;
|
||||
use super::super::helpers::result_exists;
|
||||
@@ -853,6 +852,7 @@ fn remove_else(
|
||||
TextRange::new(else_colon_end, elif_else.end()),
|
||||
desired_indentation,
|
||||
locator,
|
||||
indexer,
|
||||
stylist,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -171,4 +171,48 @@ RET505.py:200:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:207:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
205 | a = 1
|
||||
206 | return y
|
||||
207 | else:
|
||||
| ^^^^ RET505
|
||||
208 |
|
||||
209 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:217:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
215 | a = 1
|
||||
216 | return y
|
||||
217 | else:
|
||||
| ^^^^ RET505
|
||||
218 | # comment
|
||||
219 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:227:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
225 | a = 1
|
||||
226 | return y
|
||||
227 | else:
|
||||
| ^^^^ RET505
|
||||
228 | # comment
|
||||
229 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
RET505.py:237:5: RET505 Unnecessary `else` after `return` statement
|
||||
|
|
||||
235 | a = 1
|
||||
236 | return y
|
||||
237 | else:
|
||||
| ^^^^ RET505
|
||||
238 | # comment
|
||||
239 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
|
||||
|
||||
@@ -358,5 +358,107 @@ RET505.py:200:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
199 199 | if self._sb is not None: return self._sb
|
||||
200 |- else: self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
|
||||
200 |+ self._sb = '\033[01;%dm'; self._sa = '\033[0;0m';
|
||||
201 201 |
|
||||
202 202 |
|
||||
203 203 | def indent(x, y, w, z):
|
||||
|
||||
RET505.py:207:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
205 | a = 1
|
||||
206 | return y
|
||||
207 | else:
|
||||
| ^^^^ RET505
|
||||
208 |
|
||||
209 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
204 204 | if x: # [no-else-return]
|
||||
205 205 | a = 1
|
||||
206 206 | return y
|
||||
207 |- else:
|
||||
208 207 |
|
||||
209 |- c = 3
|
||||
210 |- return z
|
||||
208 |+ c = 3
|
||||
209 |+ return z
|
||||
211 210 |
|
||||
212 211 |
|
||||
213 212 | def indent(x, y, w, z):
|
||||
|
||||
RET505.py:217:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
215 | a = 1
|
||||
216 | return y
|
||||
217 | else:
|
||||
| ^^^^ RET505
|
||||
218 | # comment
|
||||
219 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
214 214 | if x: # [no-else-return]
|
||||
215 215 | a = 1
|
||||
216 216 | return y
|
||||
217 |- else:
|
||||
218 |- # comment
|
||||
219 |- c = 3
|
||||
220 |- return z
|
||||
217 |+ # comment
|
||||
218 |+ c = 3
|
||||
219 |+ return z
|
||||
221 220 |
|
||||
222 221 |
|
||||
223 222 | def indent(x, y, w, z):
|
||||
|
||||
RET505.py:227:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
225 | a = 1
|
||||
226 | return y
|
||||
227 | else:
|
||||
| ^^^^ RET505
|
||||
228 | # comment
|
||||
229 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
224 224 | if x: # [no-else-return]
|
||||
225 225 | a = 1
|
||||
226 226 | return y
|
||||
227 |- else:
|
||||
228 |- # comment
|
||||
229 |- c = 3
|
||||
230 |- return z
|
||||
227 |+ # comment
|
||||
228 |+ c = 3
|
||||
229 |+ return z
|
||||
231 230 |
|
||||
232 231 |
|
||||
233 232 | def indent(x, y, w, z):
|
||||
|
||||
RET505.py:237:5: RET505 [*] Unnecessary `else` after `return` statement
|
||||
|
|
||||
235 | a = 1
|
||||
236 | return y
|
||||
237 | else:
|
||||
| ^^^^ RET505
|
||||
238 | # comment
|
||||
239 | c = 3
|
||||
|
|
||||
= help: Remove unnecessary `else`
|
||||
|
||||
ℹ Safe fix
|
||||
234 234 | if x: # [no-else-return]
|
||||
235 235 | a = 1
|
||||
236 236 | return y
|
||||
237 |- else:
|
||||
238 237 | # comment
|
||||
239 |- c = 3
|
||||
240 |- return z
|
||||
238 |+ c = 3
|
||||
239 |+ return z
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::fix::snippet::SourceCodeSnippet;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::analyze::typing::is_dict;
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -121,6 +122,10 @@ fn is_lowercase_allowed(env_var: &str) -> bool {
|
||||
|
||||
/// SIM112
|
||||
pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::OS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ex) `os.environ['foo']`
|
||||
if let Expr::Subscript(_) = expr {
|
||||
check_os_environ_subscript(checker, expr);
|
||||
|
||||
@@ -58,6 +58,10 @@ pub(crate) fn banned_api<T: Ranged>(checker: &mut Checker, policy: &NameMatchPol
|
||||
/// TID251
|
||||
pub(crate) fn banned_attribute_access(checker: &mut Checker, expr: &Expr) {
|
||||
let banned_api = &checker.settings.flake8_tidy_imports.banned_api;
|
||||
if banned_api.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((banned_path, ban)) =
|
||||
checker
|
||||
.semantic()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -48,15 +49,17 @@ pub(crate) fn async_function_with_timeout(
|
||||
if !function_def.is_async {
|
||||
return;
|
||||
}
|
||||
|
||||
// If `trio` isn't in scope, avoid raising the diagnostic.
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the function doesn't have a `timeout` parameter, avoid raising the diagnostic.
|
||||
let Some(timeout) = function_def.parameters.find("timeout") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If `trio` isn't in scope, avoid raising the diagnostic.
|
||||
if !checker.semantic().seen(&["trio"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TrioAsyncFunctionWithTimeout,
|
||||
timeout.range(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{Expr, ExprCall};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -51,6 +52,10 @@ impl Violation for TrioSyncCall {
|
||||
|
||||
/// TRIO105
|
||||
pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(method_name) = ({
|
||||
let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else {
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::AwaitVisitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{StmtWith, WithItem};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_trio::method_name::MethodName;
|
||||
@@ -49,6 +50,10 @@ pub(crate) fn timeout_without_await(
|
||||
with_stmt: &StmtWith,
|
||||
with_items: &[WithItem],
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(method_name) = with_items.iter().find_map(|item| {
|
||||
let call = item.context_expr.as_call_expr()?;
|
||||
let call_path = checker.semantic().resolve_call_path(call.func.as_ref())?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -42,6 +43,10 @@ impl Violation for TrioUnneededSleep {
|
||||
|
||||
/// TRIO110
|
||||
pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The body should be a single `await` call.
|
||||
let [stmt] = while_stmt.body.as_slice() else {
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
|
||||
use ruff_python_semantic::analyze::typing::find_assigned_value;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -47,11 +48,7 @@ impl AlwaysFixableViolation for TrioZeroSleepCall {
|
||||
|
||||
/// TRIO115
|
||||
pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"]))
|
||||
{
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,6 +60,14 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(call.func.as_ref())
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match arg {
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
if !matches!(value, Number::Int(Int::ZERO)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||
use ruff_python_ast::{self as ast, Decorator, Expr};
|
||||
use ruff_python_codegen::{Generator, Stylist};
|
||||
use ruff_python_semantic::{
|
||||
analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel,
|
||||
analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel,
|
||||
};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -111,6 +111,10 @@ fn runtime_required_decorators(
|
||||
///
|
||||
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
|
||||
pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if !semantic.seen_module(Modules::DATACLASSES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine whether the assignment is in a `@dataclass` class definition.
|
||||
if let ScopeKind::Class(class_def) = semantic.current_scope().kind {
|
||||
if class_def.decorator_list.iter().any(|decorator| {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprAttribute};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -53,6 +54,10 @@ impl Violation for OsSepSplit {
|
||||
|
||||
/// PTH206
|
||||
pub(crate) fn os_sep_split(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::OS) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Attribute(ExprAttribute { attr, .. }) = call.func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -55,6 +56,10 @@ impl Violation for NumpyDeprecatedFunction {
|
||||
|
||||
/// NPY003
|
||||
pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::NUMPY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((existing, replacement)) =
|
||||
checker
|
||||
.semantic()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -49,6 +50,10 @@ impl Violation for NumpyDeprecatedTypeAlias {
|
||||
|
||||
/// NPY001
|
||||
pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::NUMPY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(type_name) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_python_ast::Expr;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -59,6 +60,10 @@ impl Violation for NumpyLegacyRandom {
|
||||
|
||||
/// NPY002
|
||||
pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::NUMPY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(method_name) = checker
|
||||
.semantic()
|
||||
.resolve_call_path(expr)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user