Compare commits

..

6 Commits

Author SHA1 Message Date
konstin
de239ace74 Add test cases 2023-09-22 15:50:34 +02:00
konstin
53b5121f30 . 2023-09-21 16:58:54 +02:00
konstin
be93983e8e Basic nested range formatting 2023-09-21 15:23:17 +02:00
konstin
a1239b8f2d Make basic range formatting work in vs code 2023-09-21 13:38:05 +02:00
konstin
d1b12acb3c Add byte offset indexing 2023-09-21 13:37:52 +02:00
konstin
1aabf59f77 Formatter and parser refactoring
I got confused and refactored a bit
2023-09-21 13:28:59 +02:00
621 changed files with 36074 additions and 57344 deletions

View File

@@ -9,5 +9,5 @@ updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
labels: ["internal"]

5
.github/release.yml vendored
View File

@@ -4,6 +4,7 @@ changelog:
labels:
- internal
- documentation
- formatter
categories:
- title: Breaking Changes
labels:
@@ -11,6 +12,7 @@ changelog:
- title: Rules
labels:
- rule
- autofix
- title: Settings
labels:
- configuration
@@ -18,9 +20,6 @@ changelog:
- title: Bug Fixes
labels:
- bug
- title: Formatter
labels:
- formatter
- title: Preview
labels:
- preview

View File

@@ -96,6 +96,7 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- run: pip install black[d]==23.1.0
- uses: Swatinem/rust-cache@v2
- name: "Run tests (Ubuntu)"
if: ${{ matrix.os == 'ubuntu-latest' }}
@@ -105,6 +106,10 @@ jobs:
shell: bash
# We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
run: cargo insta test --all --all-features
- run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
# TODO: Skipped as it's currently broken. The resource were moved from the
# ruff_cli to ruff crate, but this test was not updated.
if: false
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:

6
.gitignore vendored
View File

@@ -208,9 +208,3 @@ cython_debug/
# VIM
.*.sw?
.sw?
# Custom re-inclusions for the resolver test cases
!crates/ruff_python_resolver/resources/test/airflow/venv/
!crates/ruff_python_resolver/resources/test/airflow/venv/lib
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/_watchdog_fsevents.cpython-311-darwin.so
!crates/ruff_python_resolver/resources/test/airflow/venv/lib/python3.11/site-packages/orjson/orjson.cpython-311-darwin.so

View File

@@ -204,7 +204,7 @@ As such, rule names should...
For example, `AssertFalse` guards against `assert False` statements.
- _Not_ contain instructions on how to fix the violation, which instead belong in the rule
documentation and the `fix_title`.
documentation and the `autofix_title`.
- _Not_ contain a redundant prefix, like `Disallow` or `Banned`, which are already implied by the
convention.

116
Cargo.lock generated
View File

@@ -28,9 +28,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.1"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
@@ -313,9 +313,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.5"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
dependencies = [
"clap_builder",
"clap_derive",
@@ -323,9 +323,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.5"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
dependencies = [
"anstream",
"anstyle",
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.292"
version = "0.0.290"
dependencies = [
"anyhow",
"clap",
@@ -1035,9 +1035,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.7"
version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25"
checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730"
dependencies = [
"console",
"instant",
@@ -1075,9 +1075,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.33.0"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa511b2e298cd49b1856746f6bb73e17036bcd66b25f5e92cdcdbec9bd75686"
checksum = "a3e02c584f4595792d09509a94cdb92a3cef7592b1eb2d9877ee6f527062d0ea"
dependencies = [
"console",
"globset",
@@ -1260,7 +1260,8 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=03179b55ebe7e916f1722e18e8f0b87c01616d1f#03179b55ebe7e916f1722e18e8f0b87c01616d1f"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7773d520d4292e200ab1838f2daabe2feed7549f93b0a3c7582160a09e79ffde"
dependencies = [
"chic",
"libcst_derive",
@@ -1274,7 +1275,8 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=03179b55ebe7e916f1722e18e8f0b87c01616d1f#03179b55ebe7e916f1722e18e8f0b87c01616d1f"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520197c50ba477f258cd7005ec5ed3a7393693ae6bec664990c7c8d9306a7c0d"
dependencies = [
"quote",
"syn 1.0.109",
@@ -1335,9 +1337,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.6.4"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "memoffset"
@@ -1452,6 +1454,27 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
@@ -1582,9 +1605,9 @@ checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739"
[[package]]
name = "pep440_rs"
version = "0.3.12"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887f66cc62717ea72caac4f1eb4e6f392224da3ffff3f40ec13ab427802746d6"
checksum = "b05bf2c44c4cd12f03b2c3ca095f3aa21f44e43c16021c332e511884719705be"
dependencies = [
"lazy_static",
"regex",
@@ -2028,7 +2051,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.292"
version = "0.0.290"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2164,9 +2187,8 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.0.292"
version = "0.0.290"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.1",
"anyhow",
"bitflags 2.4.0",
@@ -2184,6 +2206,8 @@ dependencies = [
"log",
"memchr",
"natord",
"num-bigint",
"num-traits",
"once_cell",
"path-absolutize",
"pathdiff",
@@ -2266,6 +2290,8 @@ dependencies = [
"is-macro",
"itertools 0.11.0",
"memchr",
"num-bigint",
"num-traits",
"once_cell",
"ruff_python_parser",
"ruff_python_trivia",
@@ -2296,6 +2322,7 @@ dependencies = [
"bitflags 2.4.0",
"clap",
"countme",
"indoc",
"insta",
"itertools 0.11.0",
"memchr",
@@ -2310,7 +2337,6 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash",
"schemars",
"serde",
"serde_json",
"similar",
@@ -2342,6 +2368,7 @@ dependencies = [
"is-macro",
"itertools 0.11.0",
"lexical-parse-float",
"num-traits",
"rand",
"unic-ucd-category",
]
@@ -2351,12 +2378,13 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.4.0",
"insta",
"is-macro",
"itertools 0.11.0",
"lalrpop",
"lalrpop-util",
"num-bigint",
"num-traits",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash",
@@ -2382,6 +2410,7 @@ version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"is-macro",
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
@@ -2495,9 +2524,7 @@ dependencies = [
"ruff_formatter",
"ruff_linter",
"ruff_macros",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_source_file",
"rustc-hash",
"schemars",
"serde",
@@ -2544,10 +2571,20 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"log",
"ring",
"rustls-webpki",
"rustls-webpki 0.101.4",
"sct",
]
[[package]]
name = "rustls-webpki"
version = "0.100.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.101.4"
@@ -2633,9 +2670,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.19"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
@@ -2945,18 +2982,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.49"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.49"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
@@ -3233,16 +3270,16 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.8.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3"
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
dependencies = [
"base64",
"flate2",
"log",
"once_cell",
"rustls",
"rustls-webpki",
"rustls-webpki 0.100.2",
"url",
"webpki-roots",
]
@@ -3460,9 +3497,12 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.25.2"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki 0.100.2",
]
[[package]]
name = "which"
@@ -3477,9 +3517,9 @@ dependencies = [
[[package]]
name = "wild"
version = "2.2.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d01931a94d5a115a53f95292f51d316856b68a035618eb831bbba593a30b67"
checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74"
dependencies = [
"glob",
]

View File

@@ -15,17 +15,19 @@ license = "MIT"
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.5", features = ["derive"] }
clap = { version = "4.4.4", features = ["derive"] }
colored = { version = "2.0.0" }
filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.33.0", feature = ["filters", "glob"] }
insta = { version = "1.32.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.0" }
itertools = { version = "0.11.0" }
log = { version = "0.4.17" }
memchr = "2.6.4"
memchr = "2.6.3"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.67" }
@@ -43,7 +45,7 @@ strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }
syn = { version = "2.0.37" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.49" }
thiserror = { version = "1.0.48" }
toml = { version = "0.7.8" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
@@ -53,7 +55,8 @@ unicode-width = "0.1.11"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "03179b55ebe7e916f1722e18e8f0b87c01616d1f", default-features = false }
# v1.0.1
libcst = { version = "0.1.0", default-features = false }
[profile.release]
lto = "fat"

View File

@@ -27,10 +27,10 @@ An extremely fast Python linter, written in Rust.
- ⚡️ 10-100x faster than existing linters
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.12 compatibility
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/)
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [600 built-in rules](https://docs.astral.sh/ruff/rules/)
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.292
rev: v0.0.290
hooks:
- id: ruff
```
@@ -176,7 +176,7 @@ If left unspecified, the default configuration is equivalent to:
select = ["E", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
unfixable = []
@@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 700 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.292"
version = "0.0.290"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
@@ -23,7 +23,7 @@ configparser = { version = "3.0.2" }
itertools = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.3.1", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }

View File

@@ -16,8 +16,8 @@ use ruff_linter::settings::types::PythonVersion;
use ruff_linter::warn_user;
use ruff_workspace::options::{
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions,
McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, McCabeOptions,
Options, Pep8NamingOptions, PydocstyleOptions,
};
use ruff_workspace::pyproject::Pyproject;
@@ -103,7 +103,6 @@ pub(crate) fn convert(
// Parse each supported option.
let mut options = Options::default();
let mut lint_options = LintOptions::default();
let mut flake8_annotations = Flake8AnnotationsOptions::default();
let mut flake8_bugbear = Flake8BugbearOptions::default();
let mut flake8_builtins = Flake8BuiltinsOptions::default();
@@ -151,7 +150,7 @@ pub(crate) fn convert(
"per-file-ignores" | "per_file_ignores" => {
match parser::parse_files_to_codes_mapping(value.as_ref()) {
Ok(per_file_ignores) => {
lint_options.per_file_ignores =
options.per_file_ignores =
Some(parser::collect_per_file_ignores(per_file_ignores));
}
Err(e) => {
@@ -359,47 +358,47 @@ pub(crate) fn convert(
}
// Deduplicate and sort.
lint_options.select = Some(
options.select = Some(
select
.into_iter()
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
lint_options.ignore = Some(
options.ignore = Some(
ignore
.into_iter()
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
if flake8_annotations != Flake8AnnotationsOptions::default() {
lint_options.flake8_annotations = Some(flake8_annotations);
options.flake8_annotations = Some(flake8_annotations);
}
if flake8_bugbear != Flake8BugbearOptions::default() {
lint_options.flake8_bugbear = Some(flake8_bugbear);
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_builtins != Flake8BuiltinsOptions::default() {
lint_options.flake8_builtins = Some(flake8_builtins);
options.flake8_builtins = Some(flake8_builtins);
}
if flake8_errmsg != Flake8ErrMsgOptions::default() {
lint_options.flake8_errmsg = Some(flake8_errmsg);
options.flake8_errmsg = Some(flake8_errmsg);
}
if flake8_pytest_style != Flake8PytestStyleOptions::default() {
lint_options.flake8_pytest_style = Some(flake8_pytest_style);
options.flake8_pytest_style = Some(flake8_pytest_style);
}
if flake8_quotes != Flake8QuotesOptions::default() {
lint_options.flake8_quotes = Some(flake8_quotes);
options.flake8_quotes = Some(flake8_quotes);
}
if flake8_tidy_imports != Flake8TidyImportsOptions::default() {
lint_options.flake8_tidy_imports = Some(flake8_tidy_imports);
options.flake8_tidy_imports = Some(flake8_tidy_imports);
}
if mccabe != McCabeOptions::default() {
lint_options.mccabe = Some(mccabe);
options.mccabe = Some(mccabe);
}
if pep8_naming != Pep8NamingOptions::default() {
lint_options.pep8_naming = Some(pep8_naming);
options.pep8_naming = Some(pep8_naming);
}
if pydocstyle != PydocstyleOptions::default() {
lint_options.pydocstyle = Some(pydocstyle);
options.pydocstyle = Some(pydocstyle);
}
// Extract any settings from the existing `pyproject.toml`.
@@ -437,10 +436,6 @@ pub(crate) fn convert(
}
}
if lint_options != LintOptions::default() {
options.lint = Some(lint_options);
}
// Create the pyproject.toml.
Pyproject::new(options)
}
@@ -469,7 +464,7 @@ mod tests {
use ruff_linter::rules::flake8_quotes;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::settings::types::PythonVersion;
use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions};
use ruff_workspace::options::{Flake8QuotesOptions, Options, PydocstyleOptions};
use ruff_workspace::pyproject::Pyproject;
use crate::converter::DEFAULT_SELECTORS;
@@ -479,8 +474,8 @@ mod tests {
use super::super::plugin::Plugin;
use super::convert;
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintOptions {
LintOptions {
fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
Options {
ignore: Some(vec![]),
select: Some(
DEFAULT_SELECTORS
@@ -490,7 +485,7 @@ mod tests {
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
),
..LintOptions::default()
..Options::default()
}
}
@@ -501,10 +496,7 @@ mod tests {
&ExternalConfig::default(),
None,
);
let expected = Pyproject::new(Options {
lint: Some(lint_default_options([])),
..Options::default()
});
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
}
@@ -520,8 +512,7 @@ mod tests {
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(lint_default_options([])),
..Options::default()
..default_options([])
});
assert_eq!(actual, expected);
}
@@ -538,8 +529,7 @@ mod tests {
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(lint_default_options([])),
..Options::default()
..default_options([])
});
assert_eq!(actual, expected);
}
@@ -554,10 +544,7 @@ mod tests {
&ExternalConfig::default(),
Some(vec![]),
);
let expected = Pyproject::new(Options {
lint: Some(lint_default_options([])),
..Options::default()
});
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
}
@@ -572,16 +559,13 @@ mod tests {
Some(vec![]),
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([])
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..Options::default()
..default_options([])
});
assert_eq!(actual, expected);
}
@@ -600,15 +584,12 @@ mod tests {
Some(vec![Plugin::Flake8Docstrings]),
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
pydocstyle: Some(PydocstyleOptions {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
}),
..lint_default_options([Linter::Pydocstyle.into()])
pydocstyle: Some(PydocstyleOptions {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
}),
..Options::default()
..default_options([Linter::Pydocstyle.into()])
});
assert_eq!(actual, expected);
}
@@ -624,16 +605,13 @@ mod tests {
None,
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([Linter::Flake8Quotes.into()])
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..Options::default()
..default_options([Linter::Flake8Quotes.into()])
});
assert_eq!(actual, expected);
}
@@ -652,8 +630,7 @@ mod tests {
);
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
lint: Some(lint_default_options([])),
..Options::default()
..default_options([])
});
assert_eq!(actual, expected);

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::anyhow;
use ruff_linter::registry::Linter;
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::types::PreviewMode;
use ruff_linter::RuleSelector;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
@@ -332,7 +332,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
.filter(|plugin| {
for selector in selectors {
if selector
.rules(&PreviewOptions::default())
.rules(PreviewMode::Disabled)
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
{
return true;

View File

@@ -35,7 +35,7 @@ once_cell.workspace = true
serde.workspace = true
serde_json.workspace = true
url = "2.3.1"
ureq = "2.8.0"
ureq = "2.6.2"
criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { version="2.2.0", default-features = false, optional = true}

View File

@@ -65,7 +65,7 @@ fn benchmark_formatter(criterion: &mut Criterion) {
let comment_ranges = comment_ranges.finish();
// Parse the AST.
let module = parse_tokens(tokens, case.code(), Mode::Module, "<filename>")
let module = parse_tokens(tokens, Mode::Module, "<filename>")
.expect("Input to be a valid python program");
b.iter(|| {

View File

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

View File

@@ -0,0 +1,3 @@
a = 1
__all__ = list(["a", "b"])

View File

@@ -11,6 +11,7 @@ use ruff_linter::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_python_formatter::LspRowColumn;
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::resolver::ConfigurationTransformer;
@@ -88,7 +89,7 @@ pub struct CheckCommand {
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Show an enumeration of all fixed lint violations.
/// Show an enumeration of all autofixed lint violations.
/// Use `--no-show-fixes` to disable.
#[arg(long, overrides_with("no_show_fixes"))]
show_fixes: bool,
@@ -202,7 +203,7 @@ pub struct CheckCommand {
help_heading = "File selection"
)]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`).
/// List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
@@ -212,7 +213,7 @@ pub struct CheckCommand {
hide_possible_values = true
)]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`).
/// List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
@@ -288,7 +289,7 @@ pub struct CheckCommand {
conflicts_with = "exit_non_zero_on_fix"
)]
pub exit_zero: bool,
/// Exit with a non-zero status code if any files were modified via fix, even if no lint violations remain.
/// Exit with a non-zero status code if any files were modified via autofix, even if no lint violations remain.
#[arg(long, help_heading = "Miscellaneous", conflicts_with = "exit_zero")]
pub exit_non_zero_on_fix: bool,
/// Show counts for every rule with at least one violation.
@@ -395,6 +396,14 @@ pub struct FormatCommand {
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// Range formatting start: Zero-indexed row and zero-indexed char-based column separated by
/// colon, e.g. `1:2`
#[clap(long)]
pub start: Option<LspRowColumn>,
/// Range formatting end: Zero-indexed row and zero-indexed char-based column separated by
/// colon, e.g. `3:4`
#[clap(long)]
pub end: Option<LspRowColumn>,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
@@ -516,6 +525,8 @@ impl FormatCommand {
files: self.files,
isolated: self.isolated,
stdin_filename: self.stdin_filename,
start: self.start,
end: self.end,
},
CliOverrides {
line_length: self.line_length,
@@ -572,6 +583,8 @@ pub struct FormatArguments {
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
pub start: Option<LspRowColumn>,
pub end: Option<LspRowColumn>,
}
/// CLI settings that function as configuration overrides.
@@ -610,7 +623,7 @@ impl ConfigurationTransformer for CliOverrides {
config.cache_dir = Some(cache_dir.clone());
}
if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx {
config.lint.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
config.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
}
if let Some(exclude) = &self.exclude {
config.exclude = Some(exclude.clone());
@@ -624,7 +637,7 @@ impl ConfigurationTransformer for CliOverrides {
if let Some(fix_only) = &self.fix_only {
config.fix_only = Some(*fix_only);
}
config.lint.rule_selections.push(RuleSelection {
config.rule_selections.push(RuleSelection {
select: self.select.clone(),
ignore: self
.ignore
@@ -657,7 +670,7 @@ impl ConfigurationTransformer for CliOverrides {
config.preview = Some(*preview);
}
if let Some(per_file_ignores) = &self.per_file_ignores {
config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
}
if let Some(respect_gitignore) = &self.respect_gitignore {
config.respect_gitignore = Some(*respect_gitignore);

View File

@@ -7,12 +7,12 @@ use log::{debug, error};
use rayon::prelude::*;
use ruff_linter::linter::add_noqa_to_path;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
use crate::args::CliOverrides;
use crate::diagnostics::LintSource;
/// Add `noqa` directives to a collection of files.
pub(crate) fn add_noqa(
@@ -57,8 +57,8 @@ pub(crate) fn add_noqa(
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
let LintSource(source_kind) = match LintSource::try_from_path(path, source_type) {
Ok(Some(source)) => source,
Ok(None) => return None,
Err(e) => {
error!("Failed to extract source from {}: {e}", path.display());

View File

@@ -35,7 +35,7 @@ pub(crate) fn check(
overrides: &CliOverrides,
cache: flags::Cache,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
autofix: flags::FixMode,
) -> Result<Diagnostics> {
// Collect all the Python files to check.
let start = Instant::now();
@@ -119,7 +119,7 @@ pub(crate) fn check(
}
});
lint_path(path, package, &settings.linter, cache, noqa, fix_mode).map_err(|e| {
lint_path(path, package, &settings.linter, cache, noqa, autofix).map_err(|e| {
(Some(path.to_owned()), {
let mut error = e.to_string();
for cause in e.chain() {
@@ -198,10 +198,10 @@ fn lint_path(
settings: &LinterSettings,
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
autofix: flags::FixMode,
) -> Result<Diagnostics> {
let result = catch_unwind(|| {
crate::diagnostics::lint_path(path, package, settings, cache, noqa, fix_mode)
crate::diagnostics::lint_path(path, package, settings, cache, noqa, autofix)
});
match result {

View File

@@ -16,7 +16,7 @@ pub(crate) fn check_stdin(
pyproject_config: &PyprojectConfig,
overrides: &CliOverrides,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
autofix: flags::FixMode,
) -> Result<Diagnostics> {
if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? {
@@ -33,7 +33,7 @@ pub(crate) fn check_stdin(
stdin,
&pyproject_config.settings,
noqa,
fix_mode,
autofix,
)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)

View File

@@ -1,13 +1,12 @@
use anyhow::{anyhow, Result};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
#[allow(clippy::print_stdout)]
pub(crate) fn config(key: Option<&str>) -> Result<()> {
match key {
None => print!("{}", Options::metadata()),
Some(key) => match Options::metadata().find(key) {
Some(key) => match Options::metadata().get(key) {
None => {
return Err(anyhow!("Unknown option: {key}"));
}

View File

@@ -1,27 +1,23 @@
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use itertools::Itertools;
use log::error;
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use tracing::debug;
use ruff_diagnostics::SourceMap;
use ruff_linter::fs;
use ruff_linter::logging::LogLevel;
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};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_python_formatter::{format_module_source, FormatModuleError, PyFormatOptions};
use ruff_source_file::{find_newline, LineEnding};
use ruff_workspace::resolver::python_files_in_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::panic::{catch_unwind, PanicError};
@@ -68,23 +64,24 @@ pub(crate) fn format(
Ok(entry) => {
let path = entry.path();
let SourceType::Python(source_type) = SourceType::from(path) else {
let SourceType::Python(
source_type @ (PySourceType::Python | PySourceType::Stub),
) = SourceType::from(path)
else {
// Ignore any non-Python files.
return None;
};
let resolved_settings = resolver.resolve(path, &pyproject_config);
let options = resolved_settings.formatter.to_format_options(source_type);
debug!("Formatting {} with {:?}", path.display(), options);
Some(
match catch_unwind(|| {
format_path(path, &resolved_settings.formatter, source_type, mode)
}) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
},
)
Some(match catch_unwind(|| format_path(path, options, mode)) {
Ok(inner) => inner,
Err(error) => {
Err(FormatCommandError::Panic(Some(path.to_path_buf()), error))
}
})
}
Err(err) => Some(Err(FormatCommandError::Ignore(err))),
}
@@ -142,153 +139,32 @@ pub(crate) fn format(
#[tracing::instrument(skip_all, fields(path = %path.display()))]
fn format_path(
path: &Path,
settings: &FormatterSettings,
source_type: PySourceType,
options: PyFormatOptions,
mode: FormatMode,
) -> Result<FormatCommandResult, FormatCommandError> {
// Extract the sources from the file.
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return Ok(FormatCommandResult::Unchanged),
Err(err) => {
return Err(FormatCommandError::Read(Some(path.to_path_buf()), err));
}
let unformatted = std::fs::read_to_string(path)
.map_err(|err| FormatCommandError::Read(Some(path.to_path_buf()), err))?;
let line_ending = match find_newline(&unformatted) {
Some((_, LineEnding::Lf)) | None => ruff_formatter::printer::LineEnding::LineFeed,
Some((_, LineEnding::Cr)) => ruff_formatter::printer::LineEnding::CarriageReturn,
Some((_, LineEnding::CrLf)) => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
};
// Format the source.
match format_source(source_kind, source_type, Some(path), settings)? {
FormattedSource::Formatted(formatted) => {
if mode.is_write() {
let mut writer = File::create(path).map_err(|err| {
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
})?;
formatted
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
}
Ok(FormatCommandResult::Formatted)
}
FormattedSource::Unchanged(_) => Ok(FormatCommandResult::Unchanged),
}
}
#[derive(Debug)]
pub(crate) enum FormattedSource {
/// The source was formatted, and the [`SourceKind`] contains the transformed source code.
Formatted(SourceKind),
/// The source was unchanged, and the [`SourceKind`] contains the original source code.
Unchanged(SourceKind),
}
impl From<FormattedSource> for FormatCommandResult {
fn from(value: FormattedSource) -> Self {
match value {
FormattedSource::Formatted(_) => FormatCommandResult::Formatted,
FormattedSource::Unchanged(_) => FormatCommandResult::Unchanged,
}
}
}
impl FormattedSource {
pub(crate) fn source_kind(&self) -> &SourceKind {
match self {
FormattedSource::Formatted(source_kind) => source_kind,
FormattedSource::Unchanged(source_kind) => source_kind,
}
}
}
/// Format a [`SourceKind`], returning the transformed [`SourceKind`], or `None` if the source was
/// unchanged.
pub(crate) fn format_source(
source_kind: SourceKind,
source_type: PySourceType,
path: Option<&Path>,
settings: &FormatterSettings,
) -> 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| FormatCommandError::Format(path.map(Path::to_path_buf), err))?;
let formatted = formatted.into_code();
if formatted.len() == unformatted.len() && formatted == *unformatted {
Ok(FormattedSource::Unchanged(SourceKind::Python(unformatted)))
} else {
Ok(FormattedSource::Formatted(SourceKind::Python(formatted)))
}
}
SourceKind::IpyNotebook(notebook) => {
if !notebook.is_python_notebook() {
return Ok(FormattedSource::Unchanged(SourceKind::IpyNotebook(
notebook,
)));
}
let options = settings.to_format_options(source_type, notebook.source_code());
let mut output: Option<String> = None;
let mut last: Option<TextSize> = None;
let mut source_map = SourceMap::default();
// Format each cell individually.
for (start, end) in notebook.cell_offsets().iter().tuple_windows::<(_, _)>() {
let range = TextRange::new(*start, *end);
let unformatted = &notebook.source_code()[range];
// Format the cell.
let formatted = format_module_source(unformatted, options.clone())
.map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?;
// If the cell is unchanged, skip it.
let formatted = formatted.as_code();
if formatted.len() == unformatted.len() && formatted == unformatted {
continue;
}
// If this is the first newly-formatted cell, initialize the output.
let output = output
.get_or_insert_with(|| String::with_capacity(notebook.source_code().len()));
// Add all contents from `last` to the current cell.
let slice = &notebook.source_code()
[TextRange::new(last.unwrap_or_default(), range.start())];
output.push_str(slice);
// Add the start source marker for the cell.
source_map.push_marker(*start, output.text_len());
// Add the cell itself.
output.push_str(formatted);
// Add the end source marker for the added cell.
source_map.push_marker(*end, output.text_len());
// Track that the cell was formatted.
last = Some(*end);
}
// If the file was unchanged, return `None`.
let (Some(mut output), Some(last)) = (output, last) else {
return Ok(FormattedSource::Unchanged(SourceKind::IpyNotebook(
notebook,
)));
};
// Add the remaining content.
let slice = &notebook.source_code()[usize::from(last)..];
output.push_str(slice);
// Update the notebook.
let mut notebook = notebook.clone();
notebook.update(&source_map, output);
Ok(FormattedSource::Formatted(SourceKind::IpyNotebook(
notebook,
)))
let options = options.with_line_ending(line_ending);
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(Some(path.to_path_buf()), err))?;
let formatted = formatted.as_code();
if formatted.len() == unformatted.len() && formatted == unformatted {
Ok(FormatCommandResult::Unchanged)
} else {
if mode.is_write() {
std::fs::write(path, formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
}
Ok(FormatCommandResult::Formatted)
}
}
@@ -370,10 +246,10 @@ impl Display for FormatResultSummary {
#[derive(Error, Debug)]
pub(crate) enum FormatCommandError {
Ignore(#[from] ignore::Error),
Read(Option<PathBuf>, io::Error),
Write(Option<PathBuf>, io::Error),
FormatModule(Option<PathBuf>, FormatModuleError),
Panic(Option<PathBuf>, PanicError),
Read(Option<PathBuf>, SourceError),
Format(Option<PathBuf>, FormatModuleError),
Write(Option<PathBuf>, SourceError),
}
impl Display for FormatCommandError {
@@ -426,7 +302,7 @@ impl Display for FormatCommandError {
write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
}
}
Self::Format(path, err) => {
Self::FormatModule(path, err) => {
if let Some(path) = path {
write!(
f,

View File

@@ -1,16 +1,17 @@
use std::io::stdout;
use std::io::{stdout, Write};
use std::path::Path;
use anyhow::Result;
use log::warn;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{
format_module_source, format_module_source_range, LspRowColumn, PyFormatOptions,
};
use ruff_workspace::resolver::python_file_at_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{format_source, FormatCommandError, FormatCommandResult, FormatMode};
use crate::commands::format::{FormatCommandError, FormatCommandResult, FormatMode};
use crate::resolve::resolve;
use crate::stdin::read_from_stdin;
use crate::ExitStatus;
@@ -35,19 +36,15 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
}
}
// Format the file.
let path = cli.stdin_filename.as_deref();
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
return Ok(ExitStatus::Success);
};
let options = pyproject_config
.settings
.formatter
.to_format_options(path.map(PySourceType::from).unwrap_or_default());
// Format the file.
match format_source_code(
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
match format_source(path, options, mode, cli.start, cli.end) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check => {
@@ -66,35 +63,34 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
}
/// Format source code read from `stdin`.
fn format_source_code(
fn format_source(
path: Option<&Path>,
settings: &FormatterSettings,
source_type: PySourceType,
options: PyFormatOptions,
mode: FormatMode,
start: Option<LspRowColumn>,
end: Option<LspRowColumn>,
) -> Result<FormatCommandResult, FormatCommandError> {
// Read the source from stdin.
let source_code = read_from_stdin()
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err.into()))?;
let source_kind = match SourceKind::from_source_code(source_code, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return Ok(FormatCommandResult::Unchanged),
Err(err) => {
return Err(FormatCommandError::Read(path.map(Path::to_path_buf), err));
}
};
// Format the source.
let formatted = format_source(source_kind, source_type, path, settings)?;
// Write to stdout regardless of whether the source was formatted.
if mode.is_write() {
let mut writer = stdout().lock();
let unformatted = read_from_stdin()
.map_err(|err| FormatCommandError::Read(path.map(Path::to_path_buf), err))?;
let formatted = if start.is_some() || end.is_some() {
let formatted = format_module_source_range(&unformatted, options, start, end)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
formatted
.source_kind()
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
} else {
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::FormatModule(path.map(Path::to_path_buf), err))?;
let formatted = formatted.as_code();
formatted.to_string()
};
if formatted.len() == unformatted.len() && formatted == unformatted {
Ok(FormatCommandResult::Unchanged)
} else {
if mode.is_write() {
stdout()
.lock()
.write_all(formatted.as_bytes())
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
Ok(FormatCommandResult::Formatted)
}
Ok(FormatCommandResult::from(formatted))
}

View File

@@ -5,7 +5,7 @@ use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use crate::args::HelpFormat;
@@ -17,7 +17,7 @@ struct Explanation<'a> {
linter: &'a str,
summary: &'a str,
message_formats: &'a [&'a str],
fix: String,
autofix: String,
explanation: Option<&'a str>,
preview: bool,
}
@@ -26,14 +26,14 @@ impl<'a> Explanation<'a> {
fn from_rule(rule: &'a Rule) -> Self {
let code = rule.noqa_code().to_string();
let (linter, _) = Linter::parse_code(&code).unwrap();
let fix = rule.fixable().to_string();
let autofix = rule.autofixable().to_string();
Self {
name: rule.as_ref(),
code,
linter: linter.name(),
summary: rule.message_formats()[0],
message_formats: rule.message_formats(),
fix,
autofix,
explanation: rule.explanation(),
preview: rule.is_preview(),
}
@@ -51,12 +51,9 @@ fn format_rule_text(rule: Rule) -> String {
output.push('\n');
output.push('\n');
let fix_availability = rule.fixable();
if matches!(
fix_availability,
FixAvailability::Always | FixAvailability::Sometimes
) {
output.push_str(&fix_availability.to_string());
let autofix = rule.autofixable();
if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) {
output.push_str(&autofix.to_string());
output.push('\n');
output.push('\n');
}

View File

@@ -8,12 +8,13 @@ use std::ops::AddAssign;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use colored::Colorize;
use filetime::FileTime;
use log::{debug, error, warn};
use rustc_hash::FxHashMap;
use similar::TextDiff;
use thiserror::Error;
use ruff_diagnostics::Diagnostic;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
@@ -22,12 +23,12 @@ use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::registry::AsRule;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{fs, IOError, SyntaxError};
use ruff_macros::CacheKey;
use ruff_notebook::{Cell, Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::{SourceType, TomlSourceType};
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::Settings;
@@ -81,38 +82,15 @@ impl Diagnostics {
}
}
/// Generate [`Diagnostics`] based on a [`SourceError`].
/// Generate [`Diagnostics`] based on a [`SourceExtractionError`].
pub(crate) fn from_source_error(
err: &SourceError,
err: &SourceExtractionError,
path: Option<&Path>,
settings: &LinterSettings,
) -> Self {
let diagnostic = match err {
// IO errors.
SourceError::Io(_)
| SourceError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
)
}
// Syntax errors.
SourceError::Notebook(
NotebookError::InvalidJson(_)
| NotebookError::InvalidSchema(_)
| NotebookError::InvalidFormat(_),
) => Diagnostic::new(
SyntaxError {
message: err.to_string(),
},
TextRange::default(),
),
};
let diagnostic = Diagnostic::from(err);
if settings.rules.enabled(diagnostic.kind.rule()) {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let name = path.map_or_else(|| "-".into(), std::path::Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Message::from_diagnostic(
@@ -169,7 +147,7 @@ pub(crate) fn lint_path(
settings: &LinterSettings,
cache: Option<&Cache>,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
autofix: flags::FixMode,
) -> Result<Diagnostics> {
// Check the cache.
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
@@ -178,7 +156,7 @@ pub(crate) fn lint_path(
// write the fixes to disk, thus invalidating the cache. But it's a bit hard
// to reason about. We need to come up with a better solution here.)
let caching = match cache {
Some(cache) if noqa.into() && fix_mode.is_generate() => {
Some(cache) if noqa.into() && autofix.is_generate() => {
let relative_path = cache
.relative_path(path)
.expect("wrong package cache for file");
@@ -205,12 +183,13 @@ pub(crate) fn lint_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_pyproject_toml())
{
let contents = match std::fs::read_to_string(path).map_err(SourceError::from) {
Ok(contents) => contents,
Err(err) => {
return Ok(Diagnostics::from_source_error(&err, Some(path), settings));
}
};
let contents =
match std::fs::read_to_string(path).map_err(SourceExtractionError::Io) {
Ok(contents) => contents,
Err(err) => {
return Ok(Diagnostics::from_source_error(&err, Some(path), settings));
}
};
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
lint_pyproject_toml(source_file, settings)
} else {
@@ -226,8 +205,8 @@ pub(crate) fn lint_path(
};
// Extract the sources from the file.
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
let LintSource(source_kind) = match LintSource::try_from_path(path, source_type) {
Ok(Some(sources)) => sources,
Ok(None) => return Ok(Diagnostics::default()),
Err(err) => {
return Ok(Diagnostics::from_source_error(&err, Some(path), settings));
@@ -241,7 +220,7 @@ pub(crate) fn lint_path(
error: parse_error,
},
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
) = if matches!(autofix, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
@@ -249,7 +228,7 @@ pub(crate) fn lint_path(
}) = lint_fix(path, package, noqa, settings, &source_kind, source_type)
{
if !fixed.is_empty() {
match fix_mode {
match autofix {
flags::FixMode::Apply => match transformed.as_ref() {
SourceKind::Python(transformed) => {
write(path, transformed.as_bytes())?;
@@ -275,9 +254,10 @@ pub(crate) fn lint_path(
// mutated it.
let src_notebook = source_kind.as_ipy_notebook().unwrap();
let mut stdout = io::stdout().lock();
// Cell indices are 1-based.
for ((idx, src_cell), dest_cell) in (1u32..)
.zip(src_notebook.cells().iter())
for ((idx, src_cell), dest_cell) in src_notebook
.cells()
.iter()
.enumerate()
.zip(dest_notebook.cells().iter())
{
let (Cell::Code(src_code_cell), Cell::Code(dest_code_cell)) =
@@ -322,7 +302,7 @@ pub(crate) fn lint_path(
}
(result, fixed)
} else {
// If we fail to fix, lint the original source code.
// If we fail to autofix, lint the original source code.
let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
let fixed = FxHashMap::default();
(result, fixed)
@@ -363,7 +343,13 @@ pub(crate) fn lint_path(
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
FxHashMap::from_iter([(
path.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
.to_string(),
// Index needs to be computed always to store in cache.
notebook.index().clone(),
)])
} else {
FxHashMap::default()
};
@@ -384,7 +370,7 @@ pub(crate) fn lint_stdin(
contents: String,
settings: &Settings,
noqa: flags::Noqa,
fix_mode: flags::FixMode,
autofix: flags::FixMode,
) -> Result<Diagnostics> {
// TODO(charlie): Support `pyproject.toml`.
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
@@ -392,8 +378,8 @@ pub(crate) fn lint_stdin(
};
// Extract the sources from the file.
let source_kind = match SourceKind::from_source_code(contents, source_type) {
Ok(Some(source_kind)) => source_kind,
let LintSource(source_kind) = match LintSource::try_from_source_code(contents, source_type) {
Ok(Some(sources)) => sources,
Ok(None) => return Ok(Diagnostics::default()),
Err(err) => {
return Ok(Diagnostics::from_source_error(&err, path, &settings.linter));
@@ -407,7 +393,7 @@ pub(crate) fn lint_stdin(
error: parse_error,
},
fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
) = if matches!(autofix, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult {
result,
transformed,
@@ -420,10 +406,10 @@ pub(crate) fn lint_stdin(
&source_kind,
source_type,
) {
match fix_mode {
match autofix {
flags::FixMode::Apply => {
// Write the contents to stdout, regardless of whether any errors were fixed.
transformed.write(&mut io::stdout().lock())?;
io::stdout().write_all(transformed.source_code().as_bytes())?;
}
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
@@ -449,7 +435,7 @@ pub(crate) fn lint_stdin(
(result, fixed)
} else {
// If we fail to fix, lint the original source code.
// If we fail to autofix, lint the original source code.
let result = lint_only(
path.unwrap_or_else(|| Path::new("-")),
package,
@@ -461,8 +447,8 @@ pub(crate) fn lint_stdin(
let fixed = FxHashMap::default();
// Write the contents to stdout anyway.
if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?;
if autofix.is_apply() {
io::stdout().write_all(source_kind.source_code().as_bytes())?;
}
(result, fixed)
@@ -489,15 +475,6 @@ pub(crate) fn lint_stdin(
);
}
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
FxHashMap::from_iter([(
path.map_or_else(|| "-".into(), |path| path.to_string_lossy().to_string()),
notebook.into_index(),
)])
} else {
FxHashMap::default()
};
Ok(Diagnostics {
messages,
fixed: FxHashMap::from_iter([(
@@ -505,6 +482,83 @@ pub(crate) fn lint_stdin(
fixed,
)]),
imports,
notebook_indexes,
notebook_indexes: FxHashMap::default(),
})
}
#[derive(Debug)]
pub(crate) struct LintSource(pub(crate) SourceKind);
impl LintSource {
/// Extract the lint [`LintSource`] from the given file path.
pub(crate) fn try_from_path(
path: &Path,
source_type: PySourceType,
) -> Result<Option<LintSource>, SourceExtractionError> {
if source_type.is_ipynb() {
let notebook = Notebook::from_path(path)?;
Ok(notebook
.is_python_notebook()
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
} else {
// This is tested by ruff_cli integration test `unreadable_file`
let contents = std::fs::read_to_string(path)?;
Ok(Some(LintSource(SourceKind::Python(contents))))
}
}
/// Extract the lint [`LintSource`] from the raw string contents, optionally accompanied by a
/// file path indicating the path to the file from which the contents were read. If provided,
/// the file path should be used for diagnostics, but not for reading the file from disk.
pub(crate) fn try_from_source_code(
source_code: String,
source_type: PySourceType,
) -> Result<Option<LintSource>, SourceExtractionError> {
if source_type.is_ipynb() {
let notebook = Notebook::from_source_code(&source_code)?;
Ok(notebook
.is_python_notebook()
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
} else {
Ok(Some(LintSource(SourceKind::Python(source_code))))
}
}
}
#[derive(Error, Debug)]
pub(crate) enum SourceExtractionError {
/// The extraction failed due to an [`io::Error`].
#[error(transparent)]
Io(#[from] io::Error),
/// The extraction failed due to a [`NotebookError`].
#[error(transparent)]
Notebook(#[from] NotebookError),
}
impl From<&SourceExtractionError> for Diagnostic {
fn from(err: &SourceExtractionError) -> Self {
match err {
// IO errors.
SourceExtractionError::Io(_)
| SourceExtractionError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
Diagnostic::new(
IOError {
message: err.to_string(),
},
TextRange::default(),
)
}
// Syntax errors.
SourceExtractionError::Notebook(
NotebookError::InvalidJson(_)
| NotebookError::InvalidSchema(_)
| NotebookError::InvalidFormat(_),
) => Diagnostic::new(
SyntaxError {
message: err.to_string(),
},
TextRange::default(),
),
}
}
}

View File

@@ -234,13 +234,13 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
..
} = pyproject_config.settings;
// Fix rules are as follows:
// Autofix rules are as follows:
// - By default, generate all fixes, but don't apply them to the filesystem.
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
// print them to stdout, if we're reading from stdin).
// - If `--diff` or `--fix-only` are set, don't print any violations (only
// fixes).
let fix_mode = if cli.diff {
let autofix = if cli.diff {
flags::FixMode::Diff
} else if fix || fix_only {
flags::FixMode::Apply
@@ -275,7 +275,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
}
if cli.add_noqa {
if !fix_mode.is_generate() {
if !autofix.is_generate() {
warn_user!("--fix is incompatible with --add-noqa.");
}
let modifications =
@@ -290,7 +290,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
return Ok(ExitStatus::Success);
}
let printer = Printer::new(output_format, log_level, fix_mode, printer_flags);
let printer = Printer::new(output_format, log_level, autofix, printer_flags);
if cli.watch {
if output_format != SerializationFormat::Text {
@@ -317,7 +317,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
&overrides,
cache.into(),
noqa.into(),
fix_mode,
autofix,
)?;
printer.write_continuously(&mut writer, &messages)?;
@@ -349,7 +349,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
&overrides,
cache.into(),
noqa.into(),
fix_mode,
autofix,
)?;
printer.write_continuously(&mut writer, &messages)?;
}
@@ -366,7 +366,7 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
&pyproject_config,
&overrides,
noqa.into(),
fix_mode,
autofix,
)?
} else {
commands::check::check(
@@ -375,14 +375,14 @@ pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
&overrides,
cache.into(),
noqa.into(),
fix_mode,
autofix,
)?
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff)) {
if !(is_stdin && matches!(autofix, flags::FixMode::Apply | flags::FixMode::Diff)) {
if cli.statistics {
printer.write_statistics(&diagnostics, &mut writer)?;
} else {

View File

@@ -72,7 +72,7 @@ impl From<Rule> for SerializeRuleAsCode {
pub(crate) struct Printer {
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
autofix_level: flags::FixMode,
flags: Flags,
}
@@ -80,13 +80,13 @@ impl Printer {
pub(crate) const fn new(
format: SerializationFormat,
log_level: LogLevel,
fix_mode: flags::FixMode,
autofix_level: flags::FixMode,
flags: Flags,
) -> Self {
Self {
format,
log_level,
fix_mode,
autofix_level,
flags,
}
}
@@ -118,7 +118,7 @@ impl Printer {
writeln!(writer, "Found {remaining} error{s}.")?;
}
if show_fix_status(self.fix_mode) {
if show_fix_status(self.autofix_level) {
let num_fixable = diagnostics
.messages
.iter()
@@ -140,7 +140,7 @@ impl Printer {
.sum::<usize>();
if fixed > 0 {
let s = if fixed == 1 { "" } else { "s" };
if self.fix_mode.is_apply() {
if self.autofix_level.is_apply() {
writeln!(writer, "Fixed {fixed} error{s}.")?;
} else {
writeln!(writer, "Would fix {fixed} error{s}.")?;
@@ -191,7 +191,7 @@ impl Printer {
}
SerializationFormat::Text => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.autofix_level))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.emit(writer, &diagnostics.messages, &context)?;
@@ -209,7 +209,7 @@ impl Printer {
SerializationFormat::Grouped => {
GroupedEmitter::default()
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.autofix_level))
.emit(writer, &diagnostics.messages, &context)?;
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
@@ -366,7 +366,7 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode))
.with_show_fix_status(show_fix_status(self.autofix_level))
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.emit(writer, &diagnostics.messages, &context)?;
}
@@ -390,13 +390,13 @@ fn num_digits(n: usize) -> usize {
}
/// Return `true` if the [`Printer`] should indicate that a rule is fixable.
const fn show_fix_status(fix_mode: flags::FixMode) -> bool {
const fn show_fix_status(autofix_level: flags::FixMode) -> bool {
// If we're in application mode, avoid indicating that a rule is fixable.
// If the specific violation were truly fixable, it would've been fixed in
// this pass! (We're occasionally unable to determine whether a specific
// violation is fixable without trying to fix it, so if fix is not
// violation is fixable without trying to fix it, so if autofix is not
// enabled, we may inadvertently indicate that a rule is fixable.)
!fix_mode.is_apply()
!autofix_level.is_apply()
}
fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>) -> Result<()> {

View File

@@ -0,0 +1,204 @@
#![cfg(not(target_family = "wasm"))]
use std::io::{ErrorKind, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread::sleep;
use std::time::Duration;
use std::{fs, process, str};
use anyhow::{anyhow, bail, Context, Result};
use insta_cmd::get_cargo_bin;
use log::info;
use walkdir::WalkDir;
use ruff_linter::logging::{set_up_logging, LogLevel};
/// Handles `blackd` process and allows submitting code to it for formatting.
struct Blackd {
address: SocketAddr,
server: process::Child,
client: ureq::Agent,
}
const BIN_NAME: &str = "ruff";
impl Blackd {
pub(crate) fn new() -> Result<Self> {
// Get free TCP port to run on
let address = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))?.local_addr()?;
let args = [
"--bind-host",
&address.ip().to_string(),
"--bind-port",
&address.port().to_string(),
];
let server = Command::new("blackd")
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("Starting blackd")?;
// Wait up to four seconds for `blackd` to be ready.
for _ in 0..20 {
match TcpStream::connect(address) {
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
info!("`blackd` not ready yet; retrying...");
sleep(Duration::from_millis(200));
}
Err(e) => return Err(e.into()),
Ok(_) => {
info!("`blackd` ready");
return Ok(Self {
address,
server,
client: ureq::agent(),
});
}
}
}
bail!("blackd {:?} failed to start", args)
}
/// Format given code with blackd.
pub(crate) fn check(&self, code: &[u8]) -> Result<Vec<u8>> {
match self
.client
.post(&format!("http://{}/", self.address))
.set("X-Line-Length", "88")
.send_bytes(code)
{
// 204 indicates the input wasn't changed during formatting, so
// we return the original.
Ok(response) => {
if response.status() == 204 {
Ok(code.to_vec())
} else {
let mut buf = vec![];
response
.into_reader()
.take((1024 * 1024) as u64)
.read_to_end(&mut buf)?;
Ok(buf)
}
}
Err(ureq::Error::Status(_, response)) => Err(anyhow::anyhow!(
"Formatting with `black` failed: {}",
response.into_string()?
)),
Err(e) => Err(e.into()),
}
}
}
impl Drop for Blackd {
fn drop(&mut self) {
self.server.kill().expect("Couldn't end `blackd` process");
}
}
fn run_test(path: &Path, blackd: &Blackd, ruff_args: &[&str]) -> Result<()> {
let input = fs::read(path)?;
// Step 1: Run `ruff` on the input.
let mut step_1 = Command::new(get_cargo_bin(BIN_NAME))
.args(ruff_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = step_1.stdin.take() {
stdin.write_all(input.as_ref())?;
}
let step_1_output = step_1.wait_with_output()?;
if !step_1_output.status.success() {
return Err(anyhow!(
"Running input through ruff failed:\n{}",
str::from_utf8(&step_1_output.stderr)?
));
}
// Step 2: Run `blackd` on the input.
let step_2_output = blackd.check(&step_1_output.stdout.clone())?;
// Step 3: Re-run `ruff` on the input.
let mut step_3 = Command::new(get_cargo_bin(BIN_NAME))
.args(ruff_args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = step_3.stdin.take() {
stdin.write_all(step_2_output.as_ref())?;
}
let step_3_output = step_3.wait_with_output()?;
if !step_3_output.status.success() {
return Err(anyhow!(
"Running input through ruff after black failed:\n{}",
str::from_utf8(&step_3_output.stderr)?
));
}
let step_3_output = step_3_output.stdout.clone();
assert_eq!(
str::from_utf8(&step_2_output),
str::from_utf8(&step_3_output),
"Mismatch found for {}",
path.display()
);
Ok(())
}
#[test]
#[ignore]
fn test_ruff_black_compatibility() -> Result<()> {
set_up_logging(&LogLevel::Default)?;
let blackd = Blackd::new()?;
let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("test")
.join("fixtures");
// Ignore some fixtures that currently trigger errors. `E999.py` especially, as
// that is triggering a syntax error on purpose.
let excludes = ["E999.py", "W605_1.py"];
let paths = WalkDir::new(fixtures_dir)
.into_iter()
.flatten()
.filter(|entry| {
entry
.path()
.extension()
.is_some_and(|ext| ext == "py" || ext == "pyi")
&& !excludes.contains(&entry.path().file_name().unwrap().to_str().unwrap())
});
let ruff_args = [
"-",
"--silent",
"--exit-zero",
"--fix",
"--line-length",
"88",
"--select",
"ALL",
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
"--ignore",
"RUF",
];
for entry in paths {
let path = entry.path();
run_test(path, &blackd, &ruff_args).context(format!("Testing {}", path.display()))?;
}
Ok(())
}

View File

@@ -1,207 +0,0 @@
#![cfg(not(target_family = "wasm"))]
use std::fs;
use std::process::Command;
use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print('Should\'t change quotes')
if condition:
print('Hy "Micha"') # Should not change quotes
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Should't change quotes")
if condition:
print('Hy "Micha"') # Should not change quotes
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
}
#[test]
fn format_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[format]
indent-style = "tab"
quote-style = "single"
skip-magic-trailing-comma = true
line-ending = "cr-lf"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(arg1, arg2):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
#[test]
fn format_option_inheritance() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
let base_toml = tempdir.path().join("base.toml");
fs::write(
&ruff_toml,
r#"
extend = "base.toml"
[format]
quote-style = "single"
"#,
)?;
fs::write(
base_toml,
r#"
[format]
indent-style = "tab"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
if condition:
print("Should change quotes")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def foo(
arg1,
arg2,
):
print("Shouldn't change quotes")
if condition:
print('Should change quotes')
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
Ok(())
}
/// Tests that the legacy `format` option continues to work but emits a warning.
#[test]
fn legacy_format_option() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
format = "json"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "F401", "--no-cache", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
[
{
"code": "F401",
"end_location": {
"column": 10,
"row": 2
},
"filename": "-",
"fix": {
"applicability": "Automatic",
"edits": [
{
"content": "",
"end_location": {
"column": 1,
"row": 3
},
"location": {
"column": 1,
"row": 2
}
}
],
"message": "Remove unused import: `os`"
},
"location": {
"column": 8,
"row": 2
},
"message": "`os` imported but unused",
"noqa_row": 2,
"url": "https://docs.astral.sh/ruff/rules/unused-import"
}
]
----- stderr -----
warning: The option `format` has been deprecated to avoid ambiguity with Ruff's upcoming formatter. Use `output-format` instead.
"###);
Ok(())
}

View File

@@ -12,7 +12,8 @@ use std::process::Command;
use std::str;
#[cfg(unix)]
use anyhow::{Context, Result};
use anyhow::Context;
use anyhow::Result;
#[cfg(unix)]
use clap::Parser;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
@@ -75,8 +76,8 @@ fn stdin_filename() {
"###);
}
/// Raise `TCH` errors in `.py` files ...
#[test]
/// Raise `TCH` errors in `.py` files ...
fn stdin_source_type_py() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
@@ -136,7 +137,7 @@ fn stdin_json() {
}
#[test]
fn stdin_fix_py() {
fn stdin_autofix() {
let args = ["--fix"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
@@ -154,174 +155,7 @@ fn stdin_fix_py() {
}
#[test]
fn stdin_fix_jupyter() {
let args = ["--fix", "--stdin-filename", "Jupyter.ipynb"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(args)
.pass_stdin(r#"{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
"metadata": {},
"outputs": [],
"source": [
"import os"
]
},
{
"cell_type": "markdown",
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
"metadata": {},
"source": [
"Foo"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
"metadata": {},
"outputs": [],
"source": [
"import sys"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e40b33d2-7fe4-46c5-bdf0-8802f3052565",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1\n"
]
}
],
"source": [
"print(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1899bc8-d46f-4ec0-b1d1-e1ca0f04bf60",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}"#), @r###"
success: true
exit_code: 0
----- stdout -----
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
"metadata": {},
"source": [
"Foo"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e40b33d2-7fe4-46c5-bdf0-8802f3052565",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1\n"
]
}
],
"source": [
"print(1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1899bc8-d46f-4ec0-b1d1-e1ca0f04bf60",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.11.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
----- stderr -----
"###);
}
#[test]
fn stdin_fix_when_not_fixable_should_still_print_contents() {
fn stdin_autofix_when_not_fixable_should_still_print_contents() {
let args = ["--fix"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
@@ -340,7 +174,7 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
}
#[test]
fn stdin_fix_when_no_issues_should_still_print_contents() {
fn stdin_autofix_when_no_issues_should_still_print_contents() {
let args = ["--fix"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
@@ -357,135 +191,6 @@ fn stdin_fix_when_no_issues_should_still_print_contents() {
"###);
}
#[test]
fn stdin_format_jupyter() {
let args = ["format", "--stdin-filename", "Jupyter.ipynb", "--isolated"];
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(args)
.pass_stdin(r#"{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
"metadata": {},
"outputs": [],
"source": [
"x=1"
]
},
{
"cell_type": "markdown",
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
"metadata": {},
"source": [
"Foo"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
"metadata": {},
"outputs": [],
"source": [
"def func():\n",
" pass\n",
"print(1)\n",
"import os"
]
}
],
"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.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#), @r###"
success: true
exit_code: 0
----- stdout -----
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "dccc687c-96e2-4604-b957-a8a89b5bec06",
"metadata": {},
"outputs": [],
"source": [
"x = 1"
]
},
{
"cell_type": "markdown",
"id": "19e1b029-f516-4662-a9b9-623b93edac1a",
"metadata": {},
"source": [
"Foo"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cdce7b92-b0fb-4c02-86f6-e233b26fa84f",
"metadata": {},
"outputs": [],
"source": [
"def func():\n",
" pass\n",
"\n",
"\n",
"print(1)\n",
"import os"
]
}
],
"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.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
----- stderr -----
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
"###);
}
#[test]
fn show_source() {
let args = ["--show-source"];

View File

@@ -1,157 +0,0 @@
//! Tests the interaction of the `lint` configuration section
#![cfg(not(target_family = "wasm"))]
use std::fs;
use std::process::Command;
use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "text"];
#[test]
fn top_level_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:5: Q000 [*] Double quotes found but single quotes preferred
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn lint_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
extend-select = ["B", "Q"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:5: Q000 [*] Double quotes found but single quotes preferred
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
----- stderr -----
"###);
Ok(())
}
/// Tests that configurations from the top-level and `lint` section are merged together.
#[test]
fn mixed_levels() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:5: Q000 [*] Double quotes found but single quotes preferred
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
----- stderr -----
"###);
Ok(())
}
/// Tests that options in the `lint` section have higher precedence than top-level options (because they are more specific).
#[test]
fn precedence() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
extend-select = ["B", "Q"]
[flake8-quotes]
inline-quotes = "double"
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:5: Q000 [*] Double quotes found but single quotes preferred
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 potentially fixable with the --fix option.
----- stderr -----
"###);
Ok(())
}

View File

@@ -13,7 +13,7 @@ exit_code: 0
Derived from the **Pyflakes** linter.
Fix is sometimes available.
Autofix is sometimes available.
## What it does
Checks for unused imports.

View File

@@ -6,7 +6,7 @@ info:
- "-"
- "--isolated"
- "--no-cache"
- "--output-format"
- "--format"
- json
- "--stdin-filename"
- F401.py

View File

@@ -28,7 +28,7 @@ ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
anyhow = { workspace = true }
clap = { workspace = true }
ignore = { workspace = true }
indicatif = "0.17.7"
indicatif = "0.17.5"
itertools = { workspace = true }
libcst = { workspace = true }
once_cell = { workspace = true }

View File

@@ -549,6 +549,7 @@ fn format_dir_entry(
let settings = resolver.resolve(&path, pyproject_config);
// That's a bad way of doing this but it's not worth doing something better for format_dev
// TODO(micha) use formatter settings instead
if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width);
}

View File

@@ -8,10 +8,9 @@ use anyhow::Result;
use regex::{Captures, Regex};
use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
use crate::ROOT_DIR;
@@ -37,19 +36,16 @@ pub(crate) fn main(args: &Args) -> Result<()> {
output.push('\n');
}
let fix_availability = rule.fixable();
if matches!(
fix_availability,
FixAvailability::Always | FixAvailability::Sometimes
) {
output.push_str(&fix_availability.to_string());
let autofix = rule.autofixable();
if matches!(autofix, AutofixKind::Always | AutofixKind::Sometimes) {
output.push_str(&autofix.to_string());
output.push('\n');
output.push('\n');
}
if rule.is_preview() || rule.is_nursery() {
if rule.is_preview() {
output.push_str(
r#"This rule is unstable and in [preview](../preview.md). The `--preview` flag is required for use."#,
r#"This rule is in preview and is not stable. The `--preview` flag is required for use."#,
);
output.push('\n');
output.push('\n');
@@ -100,7 +96,10 @@ fn process_documentation(documentation: &str, out: &mut String) {
if let Some(rest) = line.strip_prefix("- `") {
let option = rest.trim_end().trim_end_matches('`');
assert!(Options::metadata().has(option), "unknown option {option}");
assert!(
Options::metadata().get(option).is_some(),
"unknown option {option}"
);
let anchor = option.replace('.', "-");
out.push_str(&format!("- [`{option}`][{option}]\n"));

View File

@@ -1,104 +1,22 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use std::fmt::Write;
use itertools::Itertools;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionField, OptionSet, OptionsMetadata, Visit};
use ruff_workspace::options_base::{OptionEntry, OptionField};
pub(crate) fn generate() -> String {
let mut output = String::new();
generate_set(&mut output, &Set::Toplevel(Options::metadata()));
output
}
fn generate_set(output: &mut String, set: &Set) {
if set.level() < 2 {
writeln!(output, "### {title}\n", title = set.title()).unwrap();
} else {
writeln!(output, "#### {title}\n", title = set.title()).unwrap();
}
if let Some(documentation) = set.metadata().documentation() {
output.push_str(documentation);
output.push('\n');
output.push('\n');
}
let mut visitor = CollectOptionsVisitor::default();
set.metadata().record(&mut visitor);
let (mut fields, mut sets) = (visitor.fields, visitor.groups);
fields.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
sets.sort_unstable_by(|(name, _), (name2, _)| name.cmp(name2));
// Generate the fields.
for (name, field) in &fields {
emit_field(output, name, field, set);
output.push_str("---\n\n");
}
// Generate all the sub-sets.
for (set_name, sub_set) in &sets {
generate_set(output, &Set::Named(set_name, *sub_set, set.level() + 1));
}
}
enum Set<'a> {
Toplevel(OptionSet),
Named(&'a str, OptionSet, u32),
}
impl<'a> Set<'a> {
fn name(&self) -> Option<&'a str> {
match self {
Set::Toplevel(_) => None,
Set::Named(name, _, _) => Some(name),
}
}
fn title(&self) -> &'a str {
match self {
Set::Toplevel(_) => "Top-level",
Set::Named(name, _, _) => name,
}
}
fn metadata(&self) -> &OptionSet {
match self {
Set::Toplevel(set) => set,
Set::Named(_, set, _) => set,
}
}
fn level(&self) -> u32 {
match self {
Set::Toplevel(_) => 0,
Set::Named(_, _, level) => *level,
}
}
}
fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set: &Set) {
let header_level = if parent_set.level() < 2 {
"####"
} else {
"#####"
};
// if there's a set name, we need to add it to the anchor
if let Some(set_name) = parent_set.name() {
fn emit_field(output: &mut String, name: &str, field: &OptionField, group_name: Option<&str>) {
// if there's a group name, we need to add it to the anchor
if let Some(group_name) = group_name {
// the anchor used to just be the name, but now it's the group name
// for backwards compatibility, we need to keep the old anchor
output.push_str(&format!("<span id=\"{name}\"></span>\n"));
output.push_str(&format!(
"{header_level} [`{name}`](#{set_name}-{name}) {{: #{set_name}-{name} }}\n"
"#### [`{name}`](#{group_name}-{name}) {{: #{group_name}-{name} }}\n"
));
} else {
output.push_str(&format!("{header_level} [`{name}`](#{name})\n"));
output.push_str(&format!("#### [`{name}`](#{name})\n"));
}
output.push('\n');
output.push_str(field.doc);
@@ -109,8 +27,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push('\n');
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if let Some(set_name) = parent_set.name() {
format!(".{set_name}")
if group_name.is_some() {
format!(".{}", group_name.unwrap())
} else {
String::new()
},
@@ -119,18 +37,38 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push('\n');
}
#[derive(Default)]
struct CollectOptionsVisitor {
groups: Vec<(String, OptionSet)>,
fields: Vec<(String, OptionField)>,
}
pub(crate) fn generate() -> String {
let mut output: String = "### Top-level\n\n".into();
impl Visit for CollectOptionsVisitor {
fn record_set(&mut self, name: &str, group: OptionSet) {
self.groups.push((name.to_owned(), group));
let sorted_options: Vec<_> = Options::metadata()
.into_iter()
.sorted_by_key(|(name, _)| *name)
.collect();
// Generate all the top-level fields.
for (name, entry) in &sorted_options {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, None);
output.push_str("---\n\n");
}
fn record_field(&mut self, name: &str, field: OptionField) {
self.fields.push((name.to_owned(), field));
// Generate all the sub-groups.
for (group_name, entry) in &sorted_options {
let OptionEntry::Group(fields) = entry else {
continue;
};
output.push_str(&format!("### {group_name}\n"));
output.push('\n');
for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) {
let OptionEntry::Field(field) = entry else {
continue;
};
emit_field(&mut output, name, field, Some(group_name));
output.push_str("---\n\n");
}
}
output
}

View File

@@ -5,11 +5,10 @@
use itertools::Itertools;
use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_diagnostics::AutofixKind;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_linter::upstream_categories::UpstreamCategoryAndPrefix;
use ruff_workspace::options::Options;
use ruff_workspace::options_base::OptionsMetadata;
const FIX_SYMBOL: &str = "🛠️";
const PREVIEW_SYMBOL: &str = "🧪";
@@ -20,11 +19,11 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
table_out.push_str("| ---- | ---- | ------- | ------: |");
table_out.push('\n');
for rule in rules {
let fix_token = match rule.fixable() {
FixAvailability::Always | FixAvailability::Sometimes => {
let fix_token = match rule.autofixable() {
AutofixKind::Always | AutofixKind::Sometimes => {
format!("<span style='opacity: 1'>{FIX_SYMBOL}</span>")
}
FixAvailability::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
};
let preview_token = if rule.is_preview() || rule.is_nursery() {
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
@@ -105,7 +104,10 @@ pub(crate) fn generate() -> String {
table_out.push('\n');
}
if Options::metadata().has(linter.name()) {
if Options::metadata()
.iter()
.any(|(name, _)| name == &linter.name())
{
table_out.push_str(&format!(
"For related settings, see [{}](settings.md#{}).",
linter.name(),

View File

@@ -88,7 +88,7 @@ impl Fix {
/// Create a new [`Fix`] with [automatic applicability](Applicability::Automatic) from multiple [`Edit`] elements.
pub fn automatic_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
edits.sort_by_key(Ranged::start);
Self {
edits,
applicability: Applicability::Automatic,
@@ -108,7 +108,7 @@ impl Fix {
/// Create a new [`Fix`] with [suggested applicability](Applicability::Suggested) from multiple [`Edit`] elements.
pub fn suggested_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
edits.sort_by_key(Ranged::start);
Self {
edits,
applicability: Applicability::Suggested,
@@ -128,7 +128,7 @@ impl Fix {
/// Create a new [`Fix`] with [manual applicability](Applicability::Manual) from multiple [`Edit`] elements.
pub fn manual_edits(edit: Edit, rest: impl IntoIterator<Item = Edit>) -> Self {
let mut edits: Vec<Edit> = std::iter::once(edit).chain(rest).collect();
edits.sort_by_key(|edit| (edit.start(), edit.end()));
edits.sort_by_key(Ranged::start);
Self {
edits,
applicability: Applicability::Manual,

View File

@@ -2,7 +2,7 @@ pub use diagnostic::{Diagnostic, DiagnosticKind};
pub use edit::Edit;
pub use fix::{Applicability, Fix, IsolationLevel};
pub use source_map::{SourceMap, SourceMarker};
pub use violation::{AlwaysFixableViolation, FixAvailability, Violation};
pub use violation::{AlwaysAutofixableViolation, AutofixKind, Violation};
mod diagnostic;
mod edit;

View File

@@ -46,7 +46,10 @@ impl SourceMap {
/// The `output_length` is the length of the transformed string before the
/// edit is applied.
pub fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
self.push_marker(edit.start(), output_length);
self.0.push(SourceMarker {
source: edit.start(),
dest: output_length,
});
}
/// Push the end marker for an [`Edit`].
@@ -55,18 +58,16 @@ impl SourceMap {
/// edit has been applied.
pub fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
if edit.is_insertion() {
self.push_marker(edit.start(), output_length);
self.0.push(SourceMarker {
source: edit.start(),
dest: output_length,
});
} else {
// Deletion or replacement
self.push_marker(edit.end(), output_length);
self.0.push(SourceMarker {
source: edit.end(),
dest: output_length,
});
}
}
/// Push a new marker to the sourcemap.
pub fn push_marker(&mut self, offset: TextSize, output_length: TextSize) {
self.0.push(SourceMarker {
source: offset,
dest: output_length,
});
}
}

View File

@@ -1,26 +1,26 @@
use std::fmt::{Debug, Display};
#[derive(Debug, Copy, Clone)]
pub enum FixAvailability {
pub enum AutofixKind {
Sometimes,
Always,
None,
}
impl Display for FixAvailability {
impl Display for AutofixKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FixAvailability::Sometimes => write!(f, "Fix is sometimes available."),
FixAvailability::Always => write!(f, "Fix is always available."),
FixAvailability::None => write!(f, "Fix is not available."),
AutofixKind::Sometimes => write!(f, "Autofix is sometimes available."),
AutofixKind::Always => write!(f, "Autofix is always available."),
AutofixKind::None => write!(f, "Autofix is not available."),
}
}
}
pub trait Violation: Debug + PartialEq + Eq {
/// `None` in the case an fix is never available or otherwise Some
/// [`FixAvailability`] describing the available fix.
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
/// `None` in the case an autofix is never available or otherwise Some
/// [`AutofixKind`] describing the available autofix.
const AUTOFIX: AutofixKind = AutofixKind::None;
/// The message used to describe the violation.
fn message(&self) -> String;
@@ -30,13 +30,13 @@ pub trait Violation: Debug + PartialEq + Eq {
None
}
// TODO(micha): Move `fix_title` to `Fix`, add new `advice` method that is shown as an advice.
// TODO(micha): Move `autofix_title` to `Fix`, add new `advice` method that is shown as an advice.
// Change the `Diagnostic` renderer to show the advice, and render the fix message after the `Suggested fix: <here>`
/// Returns the title for the fix. The message is also shown as an advice as part of the diagnostics.
/// Returns the title for the autofix. The message is also shown as an advice as part of the diagnostics.
///
/// Required for rules that have fixes.
fn fix_title(&self) -> Option<String> {
/// Required for rules that have autofixes.
fn autofix_title(&self) -> Option<String> {
None
}
@@ -45,8 +45,8 @@ pub trait Violation: Debug + PartialEq + Eq {
}
/// This trait exists just to make implementing the [`Violation`] trait more
/// convenient for violations that can always be fixed.
pub trait AlwaysFixableViolation: Debug + PartialEq + Eq {
/// convenient for violations that can always be autofixed.
pub trait AlwaysAutofixableViolation: Debug + PartialEq + Eq {
/// The message used to describe the violation.
fn message(&self) -> String;
@@ -55,31 +55,31 @@ pub trait AlwaysFixableViolation: Debug + PartialEq + Eq {
None
}
/// The title displayed for the available fix.
fn fix_title(&self) -> String;
/// The title displayed for the available autofix.
fn autofix_title(&self) -> String;
/// Returns the format strings used by
/// [`message`](AlwaysFixableViolation::message).
/// [`message`](AlwaysAutofixableViolation::message).
fn message_formats() -> &'static [&'static str];
}
/// A blanket implementation.
impl<V: AlwaysFixableViolation> Violation for V {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
impl<VA: AlwaysAutofixableViolation> Violation for VA {
const AUTOFIX: AutofixKind = AutofixKind::Always;
fn message(&self) -> String {
<Self as AlwaysFixableViolation>::message(self)
<Self as AlwaysAutofixableViolation>::message(self)
}
fn explanation() -> Option<&'static str> {
<Self as AlwaysFixableViolation>::explanation()
<Self as AlwaysAutofixableViolation>::explanation()
}
fn fix_title(&self) -> Option<String> {
Some(<Self as AlwaysFixableViolation>::fix_title(self))
fn autofix_title(&self) -> Option<String> {
Some(<Self as AlwaysAutofixableViolation>::autofix_title(self))
}
fn message_formats() -> &'static [&'static str] {
<Self as AlwaysFixableViolation>::message_formats()
<Self as AlwaysAutofixableViolation>::message_formats()
}
}

View File

@@ -1,6 +1,5 @@
use crate::prelude::TagKind;
use crate::GroupId;
use ruff_text_size::TextRange;
use std::error::Error;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
@@ -12,7 +11,7 @@ pub enum FormatError {
SyntaxError { message: &'static str },
/// In case range formatting failed because the provided range was larger
/// than the formatted syntax tree
RangeError { input: TextRange, tree: TextRange },
RangeError { row: usize, col: usize },
/// In case printing the document failed because it has an invalid structure.
InvalidDocument(InvalidDocumentError),
@@ -32,9 +31,9 @@ impl std::fmt::Display for FormatError {
FormatError::SyntaxError {message} => {
std::write!(fmt, "syntax error: {message}")
},
FormatError::RangeError { input, tree } => std::write!(
FormatError::RangeError { row, col } => std::write!(
fmt,
"formatting range {input:?} is larger than syntax tree {tree:?}"
"formatting range {row}:{col} is not a valid index"
),
FormatError::InvalidDocument(error) => std::write!(fmt, "Invalid document: {error}\n\n This is an internal Rome error. Please report if necessary."),
FormatError::PoorLayout => {

View File

@@ -55,11 +55,7 @@ use ruff_macros::CacheKey;
use ruff_text_size::{TextRange, TextSize};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, CacheKey)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum IndentStyle {

View File

@@ -334,7 +334,7 @@ macro_rules! best_fitting {
$crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+))
}
}}
}
}
#[cfg(test)]
mod tests {

View File

@@ -1,4 +1,5 @@
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_macros::CacheKey;
/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq, Default)]
@@ -120,7 +121,7 @@ impl SourceMapGeneration {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LineEnding {
/// Line Feed only (\n), common on Linux and macOS as well as inside git repos

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.0.292"
version = "0.0.290"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -29,7 +29,6 @@ ruff_python_parser = { path = "../ruff_python_parser" }
ruff_source_file = { path = "../ruff_source_file", features = ["serde"] }
ruff_text_size = { path = "../ruff_text_size" }
aho-corasick = { version = "1.1.1" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true }
bitflags = { workspace = true }
@@ -46,20 +45,22 @@ libcst = { workspace = true }
log = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
num-bigint = { workspace = true }
num-traits = { workspace = true }
once_cell = { workspace = true }
path-absolutize = { workspace = true, features = [
"once_cell_cache",
"use_unix_paths_on_wasm",
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.3.1", features = ["serde"] }
pyproject-toml = { version = "0.7.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { version = "1.0.19" }
semver = { version = "1.0.16" }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }

View File

@@ -158,9 +158,3 @@ class Foo:
@decorator()
def __init__(self: "Foo", foo: int):
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7711
class Class:
def __init__(self):
print(f"{self.attr=}")

View File

@@ -20,4 +20,3 @@ os.chmod(keyfile, stat.S_IRWXO | stat.S_IRWXG | stat.S_IRWXU) # Error
os.chmod("~/hidden_exec", stat.S_IXGRP) # Error
os.chmod("~/hidden_exec", stat.S_IXOTH) # OK
os.chmod("/etc/passwd", stat.S_IWOTH) # Error
os.chmod("/etc/passwd", 0o100000000) # Error

View File

@@ -1,54 +0,0 @@
from cryptography.hazmat import backends
from cryptography.hazmat.primitives.asymmetric import dsa
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
from Crypto.PublicKey import DSA as pycrypto_dsa
from Crypto.PublicKey import RSA as pycrypto_rsa
from Cryptodome.PublicKey import DSA as pycryptodomex_dsa
from Cryptodome.PublicKey import RSA as pycryptodomex_rsa
# OK
dsa.generate_private_key(key_size=2048, backend=backends.default_backend())
ec.generate_private_key(curve=ec.SECP384R1, backend=backends.default_backend())
rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=backends.default_backend()
)
pycrypto_dsa.generate(bits=2048)
pycrypto_rsa.generate(bits=2048)
pycryptodomex_dsa.generate(bits=2048)
pycryptodomex_rsa.generate(bits=2048)
dsa.generate_private_key(2048, backends.default_backend())
ec.generate_private_key(ec.SECP256K1, backends.default_backend())
rsa.generate_private_key(3, 2048, backends.default_backend())
pycrypto_dsa.generate(2048)
pycrypto_rsa.generate(2048)
pycryptodomex_dsa.generate(2048)
pycryptodomex_rsa.generate(2048)
# Errors
dsa.generate_private_key(key_size=2047, backend=backends.default_backend())
ec.generate_private_key(curve=ec.SECT163R2, backend=backends.default_backend())
rsa.generate_private_key(
public_exponent=65537, key_size=2047, backend=backends.default_backend()
)
pycrypto_dsa.generate(bits=2047)
pycrypto_rsa.generate(bits=2047)
pycryptodomex_dsa.generate(bits=2047)
pycryptodomex_rsa.generate(bits=2047)
dsa.generate_private_key(2047, backends.default_backend())
ec.generate_private_key(ec.SECT163R2, backends.default_backend())
rsa.generate_private_key(3, 2047, backends.default_backend())
pycrypto_dsa.generate(2047)
pycrypto_rsa.generate(2047)
pycryptodomex_dsa.generate(2047)
pycryptodomex_rsa.generate(2047)
# Don't crash when the size is variable.
rsa.generate_private_key(
public_exponent=65537, key_size=some_key_size, backend=backends.default_backend()
)
# Can't reliably know which curve was passed, in some cases like below.
ec.generate_private_key(
curve=curves[self.curve]["create"](self.size), backend=backends.default_backend()
)

View File

@@ -1,9 +1,7 @@
import paramiko
from paramiko import client
from paramiko.client import AutoAddPolicy, WarningPolicy
ssh_client = client.SSHClient()
ssh_client_from_paramiko = paramiko.SSHClient()
# OK
ssh_client.set_missing_host_key_policy(policy=foo)
@@ -14,12 +12,10 @@ ssh_client.set_missing_host_key_policy(foo)
# Errors
ssh_client.set_missing_host_key_policy(client.AutoAddPolicy)
ssh_client.set_missing_host_key_policy(client.WarningPolicy)
ssh_client.set_missing_host_key_policy(client.AutoAddPolicy())
ssh_client.set_missing_host_key_policy(AutoAddPolicy)
ssh_client.set_missing_host_key_policy(policy=client.AutoAddPolicy)
ssh_client.set_missing_host_key_policy(policy=client.WarningPolicy)
ssh_client.set_missing_host_key_policy(policy=WarningPolicy)
ssh_client_from_paramiko.set_missing_host_key_policy(paramiko.AutoAddPolicy)
# Unrelated
set_missing_host_key_policy(client.AutoAddPolicy)

View File

@@ -92,35 +92,3 @@ try:
pass
except Exception:
logging.error("...", exc_info=True)
from logging import error, exception
try:
pass
except Exception:
error("...")
try:
pass
except Exception:
error("...", exc_info=False)
try:
pass
except Exception:
error("...", exc_info=None)
try:
pass
except Exception:
exception("...")
try:
pass
except Exception:
error("...", exc_info=True)

View File

@@ -1,74 +0,0 @@
# Move mutable arguments below imports and docstrings
# https://github.com/astral-sh/ruff/issues/7616
def import_module_wrong(value: dict[str, str] = {}):
import os
def import_module_with_values_wrong(value: dict[str, str] = {}):
import os
return 2
def import_modules_wrong(value: dict[str, str] = {}):
import os
import sys
import itertools
def from_import_module_wrong(value: dict[str, str] = {}):
from os import path
def from_imports_module_wrong(value: dict[str, str] = {}):
from os import path
from sys import version_info
def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
import os
from sys import version_info
def import_docstring_module_wrong(value: dict[str, str] = {}):
"""Docstring"""
import os
def import_module_wrong(value: dict[str, str] = {}):
"""Docstring"""
import os; import sys
def import_module_wrong(value: dict[str, str] = {}):
"""Docstring"""
import os; import sys; x = 1
def import_module_wrong(value: dict[str, str] = {}):
"""Docstring"""
import os; import sys
def import_module_wrong(value: dict[str, str] = {}):
import os; import sys
def import_module_wrong(value: dict[str, str] = {}):
import os; import sys; x = 1
def import_module_wrong(value: dict[str, str] = {}):
import os; import sys
def import_module_wrong(value: dict[str, str] = {}): import os
def import_module_wrong(value: dict[str, str] = {}): import os; import sys
def import_module_wrong(value: dict[str, str] = {}): \
import os

View File

@@ -1,5 +0,0 @@
# Import followed by whitespace with no newline
# Same as B006_2.py, but import instead of docstring
def foobar(foor, bar={}):
import os

View File

@@ -1,5 +0,0 @@
# Import with no newline
# Same as B006_3.py, but import instead of docstring
def foobar(foor, bar={}):
import os

View File

@@ -56,11 +56,3 @@ setattr(foo.bar, r"baz", None)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
assert getattr(func, '_rpc')is True
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1732387247
getattr(*foo, "bar")
setattr(*foo, "bar", None)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739800901
getattr(self.
registration.registry, '__name__')

View File

@@ -76,15 +76,8 @@ except (ValueError, binascii.Error):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/6412
# https://github.com/astral-sh/ruff/issues/6412
try:
pass
except (ValueError, ValueError, TypeError):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739801758
try:
pas
except(re.error, re.error):
p

View File

@@ -627,7 +627,7 @@ result = function(
**{'ham': spam}
)
# Make sure the COM812 and UP034 rules don't fix simultaneously and cause a syntax error.
# Make sure the COM812 and UP034 rules don't autofix simultaneously and cause a syntax error.
the_first_one = next(
(i for i in range(10) if i // 2 == 0) # COM812 fix should include the final bracket
)

View File

@@ -59,23 +59,3 @@ _ = "abc" + "def" + foo
_ = foo + bar + "abc"
_ = "abc" + foo + bar
_ = foo + "abc" + bar
# Multiple strings nested inside a f-string
_ = f"a {'b' 'c' 'd'} e"
_ = f"""abc {"def" "ghi"} jkl"""
_ = f"""abc {
"def"
"ghi"
} jkl"""
# Nested f-strings
_ = "a" f"b {f"c" f"d"} e" "f"
_ = f"b {f"c" f"d {f"e" f"f"} g"} h"
_ = f"b {f"abc" \
f"def"} g"
# Explicitly concatenated nested f-strings
_ = f"a {f"first"
+ f"second"} d"
_ = f"a {f"first {f"middle"}"
+ f"second"} d"

View File

@@ -16,9 +16,3 @@ from logging import exception
exception("foo", exc_info=False) # LOG007
exception("foo", exc_info=True) # OK
exception = lambda *args, **kwargs: None
exception("foo", exc_info=False) # OK
exception("foo", exc_info=True) # OK

View File

@@ -16,12 +16,3 @@ from flask import current_app as app
flask.current_app.logger.info("Hello {}".format("World!"))
current_app.logger.info("Hello {}".format("World!"))
app.logger.log(logging.INFO, "Hello {}".format("World!"))
from logging import info, log
info("Hello {}".format("World!"))
log(logging.INFO, "Hello {}".format("World!"))
info("Hello {}".format("World!"))
log(logging.INFO, msg="Hello {}".format("World!"))
log(level=logging.INFO, msg="Hello {}".format("World!"))
log(msg="Hello {}".format("World!"), level=logging.INFO)

View File

@@ -2,8 +2,3 @@ import logging
logging.info("Hello %s" % "World!")
logging.log(logging.INFO, "Hello %s" % "World!")
from logging import info, log
info("Hello %s" % "World!")
log(logging.INFO, "Hello %s" % "World!")

View File

@@ -2,8 +2,3 @@ import logging
logging.info("Hello" + " " + "World!")
logging.log(logging.INFO, "Hello" + " " + "World!")
from logging import info, log
info("Hello" + " " + "World!")
log(logging.INFO, "Hello" + " " + "World!")

View File

@@ -6,7 +6,3 @@ logging.log(logging.INFO, f"Hello {name}")
_LOGGER = logging.getLogger()
_LOGGER.info(f"{__name__}")
from logging import info
info(f"{name}")
info(f"{__name__}")

View File

@@ -8,8 +8,3 @@ log.warn("Hello world!") # This shouldn't be considered as a logger candidate
logger.warn("Hello world!")
logging . warn("Hello World!")
from logging import warn, warning, exception
warn("foo")
warning("foo")
exception("foo")

View File

@@ -6,12 +6,3 @@ logging.info(
"name": "foobar",
},
)
from logging import info
info(
"Hello world!",
extra={
"name": "foobar",
},
)

View File

@@ -6,12 +6,3 @@ logging.info(
name="foobar",
),
)
from logging import info
info(
"Hello world!",
extra=dict(
name="foobar",
),
)

View File

@@ -19,24 +19,3 @@ except:
logging.error("Hello World", exc_info=False)
logging.error("Hello World", exc_info=sys.exc_info())
# G201
from logging import error
try:
pass
except:
error("Hello World", exc_info=True)
try:
pass
except:
error("Hello World", exc_info=sys.exc_info())
# OK
try:
pass
except:
error("Hello World", exc_info=False)
error("Hello World", exc_info=sys.exc_info())

View File

@@ -19,23 +19,3 @@ except:
logging.exception("Hello World", exc_info=False)
logging.exception("Hello World", exc_info=True)
# G202
from logging import exception
try:
pass
except:
exception("Hello World", exc_info=True)
try:
pass
except:
exception("Hello World", exc_info=sys.exc_info())
# OK
try:
pass
except:
exception("Hello World", exc_info=False)
exception("Hello World", exc_info=True)

View File

@@ -123,28 +123,3 @@ except NetworkError:
def foo() -> None:
pass
def foo():
print("foo")
pass
def foo():
"""A docstring."""
print("foo")
pass
for i in range(10):
pass
pass
for i in range(10):
pass
pass
for i in range(10):
pass # comment
pass

View File

@@ -33,10 +33,10 @@ message
assert not (a or not (b or c))
assert not (a or not (b and c))
# detected, but no fix for messages
# detected, but no autofix for messages
assert something and something_else, "error message"
assert not (something or something_else and something_third), "with message"
# detected, but no fix for mixed conditions (e.g. `a or b and c`)
# detected, but no autofix for mixed conditions (e.g. `a or b and c`)
assert not (something or something_else and something_third)

View File

@@ -15,29 +15,3 @@ def ok_complex_logic():
def error():
resource = acquire_resource()
yield resource
import typing
from typing import Generator
@pytest.fixture()
def ok_complex_logic() -> typing.Generator[Resource, None, None]:
if some_condition:
resource = acquire_resource()
yield resource
resource.release()
return
yield None
@pytest.fixture()
def error() -> typing.Generator[typing.Any, None, None]:
resource = acquire_resource()
yield resource
@pytest.fixture()
def error() -> Generator[Resource, None, None]:
resource = acquire_resource()
yield resource

View File

@@ -9,33 +9,3 @@ this_should_raise = (
'This is a'
'\'string\''
)
# Same as above, but with f-strings
f'This is a \'string\'' # Q003
f'This is \\ a \\\'string\'' # Q003
f'"This" is a \'string\''
f"This is a 'string'"
f"\"This\" is a 'string'"
fr'This is a \'string\''
fR'This is a \'string\''
foo = (
f'This is a'
f'\'string\'' # Q003
)
# Nested f-strings (Python 3.12+)
#
# The first one is interesting because the fix for it is valid pre 3.12:
#
# f"'foo' {'nested'}"
#
# but as the actual string itself is invalid pre 3.12, we don't catch it.
f'\'foo\' {'nested'}' # Q003
f'\'foo\' {f'nested'}' # Q003
f'\'foo\' {f'\'nested\''} \'\'' # Q003
f'normal {f'nested'} normal'
f'\'normal\' {f'nested'} normal' # Q003
f'\'normal\' {f'nested'} "double quotes"'
f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003
f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l

View File

@@ -8,32 +8,3 @@ this_should_raise = (
"This is a"
"\"string\""
)
# Same as above, but with f-strings
f"This is a \"string\""
f"'This' is a \"string\""
f'This is a "string"'
f'\'This\' is a "string"'
fr"This is a \"string\""
fR"This is a \"string\""
foo = (
f"This is a"
f"\"string\""
)
# Nested f-strings (Python 3.12+)
#
# The first one is interesting because the fix for it is valid pre 3.12:
#
# f'"foo" {"nested"}'
#
# but as the actual string itself is invalid pre 3.12, we don't catch it.
f"\"foo\" {"foo"}" # Q003
f"\"foo\" {f"foo"}" # Q003
f"\"foo\" {f"\"foo\""} \"\"" # Q003
f"normal {f"nested"} normal"
f"\"normal\" {f"nested"} normal" # Q003
f"\"normal\" {f"nested"} 'single quotes'"
f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003
f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003

View File

@@ -335,7 +335,7 @@ def foo():
return x
# Fix cases
# Autofix cases
def foo():
a = 1
b=a

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env python
try:
from __builtin__ import bytes, str, open, super, range, zip, round, int, pow, object, input
except ImportError: pass

View File

@@ -121,16 +121,3 @@ async with b as b2:
with c as c2:
async with d as d2:
f(b2, c2, d2)
# SIM117
with A() as a:
with B() as b:
type ListOrSet[T] = list[T] | set[T]
class ClassA[T: str]:
def method1(self) -> T:
...
f" something { my_dict["key"] } something else "
f"foo {f"bar {x}"} baz"

View File

@@ -33,14 +33,3 @@ with open(p) as fp:
fp.read()
open(p).close()
os.getcwdb(p)
# https://github.com/astral-sh/ruff/issues/7620
def opener(path, flags):
return os.open(path, flags, dir_fd=os.open('somedir', os.O_RDONLY))
open(p, closefd=False)
open(p, opener=opener)
open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(p, 'r', - 1, None, None, None, True, None)
open(p, 'r', - 1, None, None, None, False, opener)

View File

@@ -84,8 +84,3 @@ spam[ ~ham]
x = [ #
'some value',
]
# F-strings
f"{ {'a': 1} }"
f"{[ { {'a': 1} } ]}"
f"normal { {f"{ { [1, 2] } }" } } normal"

View File

@@ -29,16 +29,5 @@ mdtypes_template = {
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
# E231
f"{(a,b)}"
# Okay because it's hard to differentiate between the usages of a colon in a f-string
f"{a:=1}"
f"{ {'a':1} }"
f"{a:.3f}"
f"{(a:=1)}"
f"{(lambda x:x)}"
f"normal{f"{a:.3f}"}normal"
#: Okay
a = (1,

View File

@@ -48,9 +48,3 @@ def add(a: int=0, b: int =0, c: int= 0) -> int:
#: Okay
def add(a: int = _default(name='f')):
return a
# F-strings
f"{a=}"
f"{a:=1}"
f"{foo(a=1)}"
f"normal {f"{a=}"} normal"

View File

@@ -1,8 +1,6 @@
# TODO: comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# TODO(charlie): comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# TODO comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# TODO comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# FIXME: comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# FIXME comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# FIXME comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`
# FIXME(charlie): comments starting with one of the configured task-tags sometimes are longer than line-length so that you can easily find them with `git grep`

View File

@@ -1,11 +0,0 @@
# OK (88 characters)
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa" # type: ignore
# OK (88 characters)
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa"# type: ignore
# OK (88 characters)
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa" # type: ignore
# Error (89 characters)
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # type: ignore

View File

@@ -152,11 +152,3 @@ x = [
multiline string with tab in it, different lines
'''
" single line string with tab in it"
f"test{
tab_indented_should_be_flagged
} <- this tab is fine"
f"""test{
tab_indented_should_be_flagged
} <- this tab is fine"""

View File

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

View File

@@ -1,4 +0,0 @@
"""
TODO:
-
"""

View File

@@ -40,5 +40,7 @@ f"{{{{x}}}}"
""f""
''f""
(""f""r"")
f"{v:{f"0.2f"}}"
f"\{{x}}"
# To be fixed
# Error: f-string: single '}' is not allowed at line 41 column 8
# f"\{{x}}"

View File

@@ -25,7 +25,3 @@ not ''}
{2 is
not ''}
# Regression test for
if values[1is not None ] is not '-':
pass

View File

@@ -1,4 +1,4 @@
"""Test case for fixing F841 violations."""
"""Test case for autofixing F841 violations."""
def f():

View File

@@ -5,6 +5,3 @@ from warnings import warn
warnings.warn("this is ok")
warn("by itself is also ok")
logging.warning("this is fine")
logger = logging.getLogger(__name__)
logger.warning("this is fine")

View File

@@ -3,6 +3,3 @@ from logging import warn
logging.warn("this is not ok")
warn("not ok")
logger = logging.getLogger(__name__)
logger.warn("this is not ok")

View File

@@ -20,8 +20,6 @@ __all__ = foo.bar # [invalid-all-format]
__all__ = foo["bar"] # [invalid-all-format]
__all__ = (foo := bar) # [invalid-all-format]
__all__ = ["Hello"]
__all__ = ("Hello",)
@@ -43,7 +41,3 @@ __all__ = __all__ + ["Hello"]
__all__ = __all__ + multiprocessing.__all__
__all__ = list[str](["Hello", "world"])
__all__ = list[str](foo())
__all__ = (foo := ["Hello", "world"])

View File

@@ -26,29 +26,3 @@ logging.info(msg="Hello %s %s")
import warning
warning.warning("Hello %s %s", "World!")
from logging import error, info, warning
warning("Hello %s %s", "World!") # [logging-too-few-args]
# do not handle calls with kwargs (like pylint)
warning("Hello %s", "World!", "again", something="else")
warning("Hello %s", "World!")
# do not handle calls without any args
info("100% dynamic")
# do not handle calls with *args
error("Example log %s, %s", "foo", "bar", "baz", *args)
# do not handle calls with **kwargs
error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
error("%(objects)d modifications: %(modifications)d errors: %(errors)d")
info(msg="Hello %s")
info(msg="Hello %s %s")

View File

@@ -22,25 +22,3 @@ logging.info(msg="Hello", something="else")
import warning
warning.warning("Hello %s", "World!", "again")
from logging import info, error, warning
warning("Hello %s", "World!", "again") # [logging-too-many-args]
warning("Hello %s", "World!", "again", something="else")
warning("Hello %s", "World!")
# do not handle calls with *args
error("Example log %s, %s", "foo", "bar", "baz", *args)
# do not handle calls with **kwargs
error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
error("%(objects)d modifications: %(modifications)d errors: %(errors)d", {"objects": 1, "modifications": 1, "errors": 1})
info(msg="Hello")
info(msg="Hello", something="else")

View File

@@ -59,15 +59,15 @@ with open(name="foo", mode="Ub") as f:
with open(mode="Ub", name="foo") as f:
pass
open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
open(mode='Ub', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open = 1
open("foo", "U")

View File

@@ -104,12 +104,3 @@ def get_owner_id_from_mac_address():
mac_address = get_primary_mac_address()
except(IOError, OSError) as ex:
msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)
# Regression test for: https://github.com/astral-sh/ruff/issues/7580
import os
try:
pass
except os.error:
pass

View File

@@ -184,15 +184,3 @@ if sys.version_info < (3,12):
if sys.version_info <= (3,12):
print("py3")
if sys.version_info <= (3,12):
print("py3")
if sys.version_info == 10000000:
print("py3")
if sys.version_info < (3,10000000):
print("py3")
if sys.version_info <= (3,10000000):
print("py3")

View File

@@ -1,33 +0,0 @@
# Errors.
print("")
print("", sep=",")
print("", end="bar")
print("", sep=",", end="bar")
print(sep="")
print("", sep="")
print("", "", sep="")
print("", "", sep="", end="")
print("", "", sep="", end="bar")
print("", sep="", end="bar")
print(sep="", end="bar")
print("", "foo", sep="")
print("foo", "", sep="")
print("foo", "", "bar", sep="")
print("", *args)
print("", *args, sep="")
print("", **kwargs)
print(sep="\t")
# OK.
print()
print("foo")
print("", "")
print("", "foo")
print("foo", "")
print("", "", sep=",")
print("", "foo", sep=",")
print("foo", "", sep=",")
print("foo", "", "bar", "", sep=",")
print("", "", **kwargs)

View File

@@ -1,31 +0,0 @@
import pathlib
from pathlib import Path
# Errors
_ = Path().resolve()
_ = pathlib.Path().resolve()
_ = Path("").resolve()
_ = pathlib.Path("").resolve()
_ = Path(".").resolve()
_ = pathlib.Path(".").resolve()
_ = Path("", **kwargs).resolve()
_ = pathlib.Path("", **kwargs).resolve()
_ = Path(".", **kwargs).resolve()
_ = pathlib.Path(".", **kwargs).resolve()
# OK
_ = Path.cwd()
_ = pathlib.Path.cwd()
_ = Path("foo").resolve()
_ = pathlib.Path("foo").resolve()
_ = Path(".", "foo").resolve()
_ = pathlib.Path(".", "foo").resolve()
_ = Path(*args).resolve()
_ = pathlib.Path(*args).resolve()

View File

@@ -19,8 +19,3 @@ def func():
import functools, operator
sum([x, y], [])
# Regression test for: https://github.com/astral-sh/ruff/issues/7718
def func():
sum((factor.dims for factor in bases), [])

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