Compare commits
117 Commits
zanie/exte
...
zanie/tupl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98f29bdcec | ||
|
|
215b1c9f25 | ||
|
|
88bf2ac43f | ||
|
|
16e511a734 | ||
|
|
7d5122603e | ||
|
|
7d4b59d6cb | ||
|
|
40c886c3bc | ||
|
|
ee1883ac6c | ||
|
|
0c1a7ffbdc | ||
|
|
1337c4ad2a | ||
|
|
c3354ad843 | ||
|
|
b6066c6bef | ||
|
|
eb769ac03d | ||
|
|
39c04c8335 | ||
|
|
5f28912a0d | ||
|
|
05ab316609 | ||
|
|
a16c04c6db | ||
|
|
2a14b94995 | ||
|
|
c4bddb88c9 | ||
|
|
8441db69ae | ||
|
|
e304d46637 | ||
|
|
23050b3653 | ||
|
|
54bc49303f | ||
|
|
63f1122a5a | ||
|
|
d5127842d7 | ||
|
|
7dc97fd358 | ||
|
|
3936bc119a | ||
|
|
a1bec32a7b | ||
|
|
6f593cac68 | ||
|
|
823ec4cf88 | ||
|
|
087d9c705e | ||
|
|
06f71f5235 | ||
|
|
2270485c09 | ||
|
|
1bbe88f0a0 | ||
|
|
e0def9ef95 | ||
|
|
85de1e97e2 | ||
|
|
9cdf864043 | ||
|
|
9a8bd09ae6 | ||
|
|
2e36781a8a | ||
|
|
615e56906e | ||
|
|
ce290076b8 | ||
|
|
1f6b28c29c | ||
|
|
d4e9418825 | ||
|
|
89f9803f4a | ||
|
|
250b226f16 | ||
|
|
25bda80e48 | ||
|
|
88e3663dbe | ||
|
|
5a055aadb0 | ||
|
|
333aedc903 | ||
|
|
6025081825 | ||
|
|
c806b4a35a | ||
|
|
1c820b0200 | ||
|
|
1f4e87f043 | ||
|
|
db5dfd4eff | ||
|
|
c2cae71247 | ||
|
|
11e99655b8 | ||
|
|
8898906ef4 | ||
|
|
9c63d80257 | ||
|
|
d6e35f72a9 | ||
|
|
c547ec1417 | ||
|
|
b049c8651a | ||
|
|
8a48dfe3e3 | ||
|
|
c437a899f8 | ||
|
|
3fd64dac83 | ||
|
|
28bfab5eff | ||
|
|
f7790698b7 | ||
|
|
89501451d9 | ||
|
|
368cb3ee95 | ||
|
|
cbc24a3de2 | ||
|
|
fc6a3c1c71 | ||
|
|
4288d8a6f4 | ||
|
|
246830c29d | ||
|
|
f72c37ff46 | ||
|
|
071c54e89b | ||
|
|
ff3dc646b0 | ||
|
|
d6aa117949 | ||
|
|
92f935eb2b | ||
|
|
ab49eaae61 | ||
|
|
4b79f57872 | ||
|
|
097e703071 | ||
|
|
cd8e1bad64 | ||
|
|
e2b5c6ac5f | ||
|
|
c36efe254e | ||
|
|
3e7b92991b | ||
|
|
25d4ddaa60 | ||
|
|
63a5a12a41 | ||
|
|
c32f943d86 | ||
|
|
d211074f59 | ||
|
|
4ffd4ed61f | ||
|
|
a4dd1e5fad | ||
|
|
be3307e9a6 | ||
|
|
317d3dd612 | ||
|
|
f5e850745c | ||
|
|
a7d1f7e1ec | ||
|
|
88c8b47326 | ||
|
|
133a745de1 | ||
|
|
6983d96d27 | ||
|
|
3c3d9ab173 | ||
|
|
ff9fb0da54 | ||
|
|
9792b1551b | ||
|
|
d1c67f91bd | ||
|
|
dbd84c947b | ||
|
|
c2ec5f0bc9 | ||
|
|
31032f4f70 | ||
|
|
f55b724254 | ||
|
|
fd07a12a52 | ||
|
|
1ee73bdedf | ||
|
|
23b55aea30 | ||
|
|
e36afc3324 | ||
|
|
8304c41714 | ||
|
|
6f31e9c00e | ||
|
|
a6cc56fd98 | ||
|
|
0236e0751c | ||
|
|
2d0769e324 | ||
|
|
80473c3f5c | ||
|
|
4d7f90e045 | ||
|
|
75bd95e58c |
94
.github/workflows/ci.yaml
vendored
94
.github/workflows/ci.yaml
vendored
@@ -43,6 +43,7 @@ jobs:
|
||||
- "!crates/ruff_dev/**"
|
||||
- "!crates/ruff_shrinking/**"
|
||||
- scripts/*
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
formatter:
|
||||
- Cargo.toml
|
||||
@@ -57,6 +58,7 @@ jobs:
|
||||
- crates/ruff_python_parser/**
|
||||
- crates/ruff_dev/**
|
||||
- scripts/*
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
cargo-fmt:
|
||||
name: "cargo fmt"
|
||||
@@ -82,12 +84,9 @@ jobs:
|
||||
- name: "Clippy (wasm)"
|
||||
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
|
||||
|
||||
cargo-test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "cargo test | ${{ matrix.os }}"
|
||||
cargo-test-linux:
|
||||
runs-on: ubuntu-latest
|
||||
name: "cargo test (linux)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -97,40 +96,34 @@ jobs:
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests (Ubuntu)"
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --all --all-features --unreferenced reject
|
||||
- name: "Run tests (Windows)"
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
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
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
name: "cargo fuzz"
|
||||
cargo-test-windows:
|
||||
runs-on: windows-latest
|
||||
name: "cargo test (windows)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install cargo-fuzz"
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz@0.11
|
||||
- run: cargo fuzz build -s none
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
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
|
||||
|
||||
cargo-test-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -151,6 +144,22 @@ jobs:
|
||||
cd crates/ruff_wasm
|
||||
wasm-pack test --node
|
||||
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
name: "cargo fuzz"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install cargo-fuzz"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz@0.11
|
||||
- run: cargo fuzz build -s none
|
||||
|
||||
scripts:
|
||||
name: "test scripts"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -172,10 +181,10 @@ jobs:
|
||||
name: "ecosystem"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- cargo-test
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
if: github.event_name == 'pull_request' && needs.determine_changes.outputs.linter == 'true'
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
@@ -183,27 +192,48 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
name: Download Ruff binary
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
name: Download base results
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
check_artifacts: true
|
||||
|
||||
- name: Run ecosystem check
|
||||
- name: Install ruff-ecosystem
|
||||
run: |
|
||||
pip install ./python/ruff-ecosystem
|
||||
|
||||
- name: Run `ruff check` ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
scripts/check_ecosystem.py ruff ${{ steps.ruff-target.outputs.download-path }}/ruff | tee ecosystem-result
|
||||
cat ecosystem-result > $GITHUB_STEP_SUMMARY
|
||||
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check
|
||||
|
||||
cat ecosystem-result-check > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-check > ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Run `ruff format` ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
|
||||
|
||||
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format
|
||||
|
||||
cat ecosystem-result-format > $GITHUB_STEP_SUMMARY
|
||||
cat ecosystem-result-format >> ecosystem-result
|
||||
|
||||
- name: Export pull request number
|
||||
run: |
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
@@ -340,7 +370,7 @@ jobs:
|
||||
check-ruff-lsp:
|
||||
name: "test ruff-lsp"
|
||||
runs-on: ubuntu-latest
|
||||
needs: cargo-test
|
||||
needs: cargo-test-linux
|
||||
steps:
|
||||
- uses: extractions/setup-just@v1
|
||||
env:
|
||||
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,9 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.3
|
||||
|
||||
This release includes a variety of improvements to the Ruff formatter, removing several known and
|
||||
unintentional deviations from Black.
|
||||
|
||||
### Formatter
|
||||
|
||||
- Avoid space around pow for `None`, `True` and `False` ([#8189](https://github.com/astral-sh/ruff/pull/8189))
|
||||
- Avoid sorting all paths in the format command ([#8181](https://github.com/astral-sh/ruff/pull/8181))
|
||||
- Insert necessary blank line between class and leading comments ([#8224](https://github.com/astral-sh/ruff/pull/8224))
|
||||
- Avoid introducing new parentheses in annotated assignments ([#8233](https://github.com/astral-sh/ruff/pull/8233))
|
||||
- Refine the warnings about incompatible linter options ([#8196](https://github.com/astral-sh/ruff/pull/8196))
|
||||
- Add test and basic implementation for formatter preview mode ([#8044](https://github.com/astral-sh/ruff/pull/8044))
|
||||
- Refine warning about incompatible `isort` settings ([#8192](https://github.com/astral-sh/ruff/pull/8192))
|
||||
- Only omit optional parentheses for starting or ending with parentheses ([#8238](https://github.com/astral-sh/ruff/pull/8238))
|
||||
- Use source type to determine parser mode for formatting ([#8205](https://github.com/astral-sh/ruff/pull/8205))
|
||||
- Don't warn about magic trailing comma when `isort.force-single-line` is true ([#8244](https://github.com/astral-sh/ruff/pull/8244))
|
||||
- Use `SourceKind::diff` for formatter ([#8240](https://github.com/astral-sh/ruff/pull/8240))
|
||||
- Fix `fmt:off` with trailing child comment ([#8234](https://github.com/astral-sh/ruff/pull/8234))
|
||||
- Formatter parentheses support for `IpyEscapeCommand` ([#8207](https://github.com/astral-sh/ruff/pull/8207))
|
||||
|
||||
### Linter
|
||||
|
||||
- \[`pylint`\] Add buffer methods to `bad-dunder-method-name` (`PLW3201`) exclusions ([#8190](https://github.com/astral-sh/ruff/pull/8190))
|
||||
- Match rule prefixes from `external` codes setting in `unused-noqa` ([#8177](https://github.com/astral-sh/ruff/pull/8177))
|
||||
- Use `line-length` setting for isort in lieu of `pycodestyle.max-line-length` ([#8235](https://github.com/astral-sh/ruff/pull/8235))
|
||||
- Update fix for `unnecessary-paren-on-raise-exception` to unsafe for unknown types ([#8231](https://github.com/astral-sh/ruff/pull/8231))
|
||||
- Correct quick fix message for `W605` ([#8255](https://github.com/astral-sh/ruff/pull/8255))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix typo in max-doc-length documentation ([#8201](https://github.com/astral-sh/ruff/pull/8201))
|
||||
- Improve documentation around linter-formatter conflicts ([#8257](https://github.com/astral-sh/ruff/pull/8257))
|
||||
- Fix link to error suppression documentation in `unused-noqa` ([#8172](https://github.com/astral-sh/ruff/pull/8172))
|
||||
- Add `external` option to `unused-noqa` documentation ([#8171](https://github.com/astral-sh/ruff/pull/8171))
|
||||
- Add title attribute to icons ([#8060](https://github.com/astral-sh/ruff/pull/8060))
|
||||
- Clarify unsafe case in RSE102 ([#8256](https://github.com/astral-sh/ruff/pull/8256))
|
||||
- Fix skipping formatting examples ([#8210](https://github.com/astral-sh/ruff/pull/8210))
|
||||
- docs: fix name of `magic-trailing-comma` option in README ([#8200](https://github.com/astral-sh/ruff/pull/8200))
|
||||
- Add note about scope of rule changing in versioning policy ([#8169](https://github.com/astral-sh/ruff/pull/8169))
|
||||
- Document: Fix default lint rules ([#8218](https://github.com/astral-sh/ruff/pull/8218))
|
||||
- Fix a wrong setting in configuration.md ([#8186](https://github.com/astral-sh/ruff/pull/8186))
|
||||
- Fix misspelled TOML headers in the tutorial ([#8209](https://github.com/astral-sh/ruff/pull/8209))
|
||||
|
||||
## 0.1.2
|
||||
|
||||
This release includes the Beta version of the Ruff formatter — an extremely fast, Black-compatible Python formatter.
|
||||
Try it today with `ruff format`.
|
||||
Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blog/the-ruff-formatter) and [read the docs](https://docs.astral.sh/ruff/formatter/).
|
||||
|
||||
### Preview features
|
||||
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2051,7 +2051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2188,7 +2188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.1",
|
||||
@@ -2381,6 +2381,7 @@ dependencies = [
|
||||
"itertools 0.11.0",
|
||||
"lalrpop",
|
||||
"lalrpop-util",
|
||||
"memchr",
|
||||
"ruff_python_ast",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
@@ -2438,7 +2439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
# Run the Ruff linter.
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.2
|
||||
rev: v0.1.3
|
||||
hooks:
|
||||
# Run the Ruff linter.
|
||||
- id: ruff
|
||||
@@ -238,7 +238,7 @@ quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
magic-trailing-comma = "respect"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
@@ -409,6 +409,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Mypy](https://github.com/python/mypy)
|
||||
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
|
||||
- [Neon](https://github.com/neondatabase/neon)
|
||||
- [NoneBot](https://github.com/nonebot/nonebot2)
|
||||
- [ONNX](https://github.com/onnx/onnx)
|
||||
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,41 @@
|
||||
"maths = (numpy.arange(100)**2).sum()\n",
|
||||
"stats= numpy.asarray([1,2,3,4]).median()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "83a0b1b8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A markdown cell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ae12f012",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# A cell with IPython escape command\n",
|
||||
"def some_function(foo, bar):\n",
|
||||
" pass\n",
|
||||
"%matplotlib inline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "10f3bbf9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"foo = %pwd\n",
|
||||
"def some_function(foo,bar,):\n",
|
||||
" # Another cell with IPython escape command\n",
|
||||
" foo = %pwd\n",
|
||||
" print(foo)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -11,7 +11,6 @@ use itertools::Itertools;
|
||||
use log::{error, warn};
|
||||
use rayon::iter::Either::{Left, Right};
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use similar::TextDiff;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -19,12 +18,11 @@ use ruff_diagnostics::SourceMap;
|
||||
use ruff_linter::fs;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::registry::Rule;
|
||||
use ruff_linter::rules::isort;
|
||||
use ruff_linter::settings::rule_table::RuleTable;
|
||||
use ruff_linter::rules::flake8_quotes::settings::Quote;
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
|
||||
@@ -107,7 +105,7 @@ pub(crate) fn format(
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
let (mut results, mut errors): (Vec<_>, Vec<_>) = paths
|
||||
let (results, mut errors): (Vec<_>, Vec<_>) = paths
|
||||
.par_iter()
|
||||
.filter_map(|entry| {
|
||||
match entry {
|
||||
@@ -168,27 +166,6 @@ pub(crate) fn format(
|
||||
});
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Make output deterministic, at least as long as we have a path
|
||||
results.sort_unstable_by(|x, y| x.path.cmp(&y.path));
|
||||
errors.sort_by(|x, y| {
|
||||
fn get_key(error: &FormatCommandError) -> Option<&PathBuf> {
|
||||
match &error {
|
||||
FormatCommandError::Ignore(ignore) => {
|
||||
if let ignore::Error::WithPath { path, .. } = ignore {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
FormatCommandError::Panic(path, _)
|
||||
| FormatCommandError::Read(path, _)
|
||||
| FormatCommandError::Format(path, _)
|
||||
| FormatCommandError::Write(path, _) => path.as_ref(),
|
||||
}
|
||||
}
|
||||
get_key(x).cmp(&get_key(y))
|
||||
});
|
||||
|
||||
debug!(
|
||||
"Formatted {} files in {:.2?}",
|
||||
results.len() + errors.len(),
|
||||
@@ -198,15 +175,21 @@ pub(crate) fn format(
|
||||
caches.persist()?;
|
||||
|
||||
// Report on any errors.
|
||||
errors.sort_unstable_by(|a, b| a.path().cmp(&b.path()));
|
||||
|
||||
for error in &errors {
|
||||
error!("{error}");
|
||||
}
|
||||
|
||||
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
let results = FormatResults::new(results.as_slice(), mode);
|
||||
|
||||
if mode.is_diff() {
|
||||
results.write_diff(&mut stdout().lock())?;
|
||||
match mode {
|
||||
FormatMode::Write => {}
|
||||
FormatMode::Check => {
|
||||
results.write_changed(&mut stdout().lock())?;
|
||||
}
|
||||
FormatMode::Diff => {
|
||||
results.write_diff(&mut stdout().lock())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Report on the formatting changes.
|
||||
@@ -470,27 +453,51 @@ impl<'a> FormatResults<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Write a diff of the formatting changes to the given writer.
|
||||
fn write_diff(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
for result in self.results {
|
||||
if let FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
} = &result.result
|
||||
{
|
||||
let text_diff =
|
||||
TextDiff::from_lines(unformatted.source_code(), formatted.source_code());
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
unified_diff.header(
|
||||
&fs::relativize_path(&result.path),
|
||||
&fs::relativize_path(&result.path),
|
||||
);
|
||||
unified_diff.to_writer(&mut *f)?;
|
||||
}
|
||||
for (path, unformatted, formatted) in self
|
||||
.results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
if let FormatResult::Diff {
|
||||
unformatted,
|
||||
formatted,
|
||||
} = &result.result
|
||||
{
|
||||
Some((result.path.as_path(), unformatted, formatted))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|(path, _, _)| *path)
|
||||
{
|
||||
unformatted.diff(formatted, Some(path), f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a list of the files that would be changed to the given writer.
|
||||
fn write_changed(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
for path in self
|
||||
.results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
if result.result.is_formatted() {
|
||||
Some(result.path.as_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sorted_unstable()
|
||||
{
|
||||
writeln!(f, "Would reformat: {}", fs::relativize_path(path).bold())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a summary of the formatting results to the given writer.
|
||||
fn write_summary(&self, f: &mut impl Write) -> io::Result<()> {
|
||||
// Compute the number of changed and unchanged files.
|
||||
let mut changed = 0u32;
|
||||
@@ -498,14 +505,6 @@ impl<'a> FormatResults<'a> {
|
||||
for result in self.results {
|
||||
match &result.result {
|
||||
FormatResult::Formatted => {
|
||||
// If we're running in check mode, report on any files that would be formatted.
|
||||
if self.mode.is_check() {
|
||||
writeln!(
|
||||
f,
|
||||
"Would reformat: {}",
|
||||
fs::relativize_path(&result.path).bold()
|
||||
)?;
|
||||
}
|
||||
changed += 1;
|
||||
}
|
||||
FormatResult::Unchanged => unchanged += 1,
|
||||
@@ -562,6 +561,26 @@ pub(crate) enum FormatCommandError {
|
||||
Read(Option<PathBuf>, SourceError),
|
||||
Format(Option<PathBuf>, FormatModuleError),
|
||||
Write(Option<PathBuf>, SourceError),
|
||||
Diff(Option<PathBuf>, io::Error),
|
||||
}
|
||||
|
||||
impl FormatCommandError {
|
||||
fn path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::Ignore(err) => {
|
||||
if let ignore::Error::WithPath { path, .. } = err {
|
||||
Some(path.as_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Panic(path, _)
|
||||
| Self::Read(path, _)
|
||||
| Self::Format(path, _)
|
||||
| Self::Write(path, _)
|
||||
| Self::Diff(path, _) => path.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FormatCommandError {
|
||||
@@ -627,6 +646,24 @@ impl Display for FormatCommandError {
|
||||
write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
|
||||
}
|
||||
}
|
||||
Self::Diff(path, err) => {
|
||||
if let Some(path) = path {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{} {err}",
|
||||
"Failed to generate diff for ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}{} {err}",
|
||||
"Failed to generate diff".bold(),
|
||||
":".bold()
|
||||
)
|
||||
}
|
||||
}
|
||||
Self::Panic(path, err) => {
|
||||
let message = r#"This indicates a bug in Ruff. If you could open an issue at:
|
||||
|
||||
@@ -663,59 +700,110 @@ pub(super) fn warn_incompatible_formatter_settings(
|
||||
{
|
||||
let mut incompatible_rules = Vec::new();
|
||||
|
||||
for incompatible_rule in RuleTable::from_iter([
|
||||
Rule::LineTooLong,
|
||||
Rule::TabIndentation,
|
||||
Rule::IndentationWithInvalidMultiple,
|
||||
Rule::IndentationWithInvalidMultipleComment,
|
||||
Rule::OverIndented,
|
||||
Rule::IndentWithSpaces,
|
||||
for rule in [
|
||||
// The formatter might collapse implicit string concatenation on a single line.
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
// Flags missing trailing commas when all arguments are on its own line:
|
||||
// ```python
|
||||
// def args(
|
||||
// aaaaaaaa, bbbbbbbbb, cccccccccc, ddddddddd, eeeeeeee, ffffff, gggggggggggg, hhhh
|
||||
// ):
|
||||
// pass
|
||||
// ```
|
||||
Rule::MissingTrailingComma,
|
||||
Rule::ProhibitedTrailingComma,
|
||||
Rule::BadQuotesInlineString,
|
||||
Rule::BadQuotesMultilineString,
|
||||
Rule::BadQuotesDocstring,
|
||||
Rule::AvoidableEscapedQuote,
|
||||
])
|
||||
.iter_enabled()
|
||||
{
|
||||
if setting.linter.rules.enabled(incompatible_rule) {
|
||||
incompatible_rules.push(format!("'{}'", incompatible_rule.noqa_code()));
|
||||
] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Rules asserting for space indentation
|
||||
if setting.formatter.indent_style.is_tab() {
|
||||
for rule in [Rule::TabIndentation, Rule::IndentWithSpaces] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rules asserting for indent-width=4
|
||||
if setting.formatter.indent_width.value() != 4 {
|
||||
for rule in [
|
||||
Rule::IndentationWithInvalidMultiple,
|
||||
Rule::IndentationWithInvalidMultipleComment,
|
||||
] {
|
||||
if setting.linter.rules.enabled(rule) {
|
||||
incompatible_rules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !incompatible_rules.is_empty() {
|
||||
incompatible_rules.sort();
|
||||
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", incompatible_rules.join(", "));
|
||||
let mut rule_names: Vec<_> = incompatible_rules
|
||||
.into_iter()
|
||||
.map(|rule| format!("`{}`", rule.noqa_code()))
|
||||
.collect();
|
||||
rule_names.sort();
|
||||
warn!("The following rules may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.", rule_names.join(", "));
|
||||
}
|
||||
|
||||
let mut incompatible_options = Vec::new();
|
||||
|
||||
let isort_defaults = isort::settings::Settings::default();
|
||||
|
||||
if setting.linter.isort.force_single_line != isort_defaults.force_single_line {
|
||||
incompatible_options.push("'isort.force-single-line'");
|
||||
// Rules with different quote styles.
|
||||
if setting
|
||||
.linter
|
||||
.rules
|
||||
.any_enabled(&[Rule::BadQuotesInlineString, Rule::AvoidableEscapedQuote])
|
||||
{
|
||||
match (
|
||||
setting.linter.flake8_quotes.inline_quotes,
|
||||
setting.formatter.quote_style,
|
||||
) {
|
||||
(Quote::Double, QuoteStyle::Single) => {
|
||||
warn!("The `flake8-quotes.inline-quotes=\"double\"` option is incompatible with the formatter's `format.quote-style=\"single\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
|
||||
}
|
||||
(Quote::Single, QuoteStyle::Double) => {
|
||||
warn!("The `flake8-quotes.inline-quotes=\"single\"` option is incompatible with the formatter's `format.quote-style=\"double\"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `\"single\"` or `\"double\"`.");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if setting.linter.isort.force_wrap_aliases != isort_defaults.force_wrap_aliases {
|
||||
incompatible_options.push("'isort.force-wrap-aliases'");
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesMultilineString)
|
||||
&& setting.linter.flake8_quotes.multiline_quotes == Quote::Single
|
||||
{
|
||||
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.isort.lines_after_imports != isort_defaults.lines_after_imports {
|
||||
incompatible_options.push("'isort.lines-after-imports'");
|
||||
if setting.linter.rules.enabled(Rule::BadQuotesDocstring)
|
||||
&& setting.linter.flake8_quotes.docstring_quotes == Quote::Single
|
||||
{
|
||||
warn!("The `flake8-quotes.multiline-quotes=\"single\"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `\"double\"`.`");
|
||||
}
|
||||
|
||||
if setting.linter.isort.lines_between_types != isort_defaults.lines_between_types {
|
||||
incompatible_options.push("'isort.lines_between_types'");
|
||||
}
|
||||
if setting.linter.rules.enabled(Rule::UnsortedImports) {
|
||||
// The formatter removes empty lines if the value is larger than 2 but always inserts a empty line after imports.
|
||||
// Two empty lines are okay because `isort` only uses this setting for top-level imports (not in nested blocks).
|
||||
if !matches!(setting.linter.isort.lines_after_imports, 1 | 2 | -1) {
|
||||
warn!("The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).");
|
||||
}
|
||||
|
||||
if setting.linter.isort.split_on_trailing_comma != isort_defaults.split_on_trailing_comma {
|
||||
incompatible_options.push("'isort.split_on_trailing_comma'");
|
||||
}
|
||||
// Values larger than two get reduced to one line by the formatter if the import is in a nested block.
|
||||
if setting.linter.isort.lines_between_types > 1 {
|
||||
warn!("The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).");
|
||||
}
|
||||
|
||||
if !incompatible_options.is_empty() {
|
||||
warn!("The following isort options may cause conflicts when used with the formatter: {}. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.", incompatible_options.join(", "));
|
||||
// isort inserts a trailing comma which the formatter preserves, but only if `skip-magic-trailing-comma` isn't false.
|
||||
// This isn't relevant when using `force-single-line`, since isort will never include a trailing comma in that case.
|
||||
if setting.formatter.magic_trailing_comma.is_ignore()
|
||||
&& !setting.linter.isort.force_single_line
|
||||
{
|
||||
if setting.linter.isort.force_wrap_aliases {
|
||||
warn!("The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.");
|
||||
}
|
||||
|
||||
if setting.linter.isort.split_on_trailing_comma {
|
||||
warn!("The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use ruff_linter::fs;
|
||||
use similar::TextDiff;
|
||||
|
||||
use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
@@ -109,14 +107,9 @@ fn format_source_code(
|
||||
}
|
||||
FormatMode::Check => {}
|
||||
FormatMode::Diff => {
|
||||
let mut writer = stdout().lock();
|
||||
let text_diff =
|
||||
TextDiff::from_lines(source_kind.source_code(), formatted.source_code());
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
if let Some(path) = path {
|
||||
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
|
||||
}
|
||||
unified_diff.to_writer(&mut writer).unwrap();
|
||||
source_kind
|
||||
.diff(formatted, path, &mut stdout().lock())
|
||||
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
|
||||
}
|
||||
},
|
||||
FormattedSource::Unchanged => {
|
||||
|
||||
@@ -230,7 +230,9 @@ fn format_option_inheritance() -> Result<()> {
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend = "base.toml"
|
||||
extend-select = ["Q000"]
|
||||
|
||||
[lint]
|
||||
extend-select = ["COM812"]
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
@@ -273,7 +275,7 @@ if condition:
|
||||
print('Should change quotes')
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: 'Q000'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -359,15 +361,27 @@ fn conflicting_options() -> Result<()> {
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
indent-width = 2
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212"]
|
||||
|
||||
[isort]
|
||||
force-single-line = true
|
||||
force-wrap-aliases = true
|
||||
lines-after-imports = 0
|
||||
[lint.isort]
|
||||
lines-after-imports = 3
|
||||
lines-between-types = 2
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = true
|
||||
indent-style = "tab"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
@@ -389,8 +403,14 @@ def say_hy(name: str):
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: 'COM812', 'COM819', 'D206', 'E501', 'ISC001', 'Q000', 'Q001', 'Q002', 'Q003', 'W191'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The following isort options may cause conflicts when used with the formatter: 'isort.force-single-line', 'isort.force-wrap-aliases', 'isort.lines-after-imports', 'isort.lines_between_types'. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
@@ -402,15 +422,27 @@ fn conflicting_options_stdin() -> Result<()> {
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
indent-width = 2
|
||||
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212"]
|
||||
|
||||
[isort]
|
||||
force-single-line = true
|
||||
force-wrap-aliases = true
|
||||
lines-after-imports = 0
|
||||
[lint.isort]
|
||||
lines-after-imports = 3
|
||||
lines-between-types = 2
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "single"
|
||||
multiline-quotes = "single"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = true
|
||||
indent-style = "tab"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
@@ -425,14 +457,110 @@ def say_hy(name: str):
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")
|
||||
print(f"Hy {name}")
|
||||
|
||||
----- stderr -----
|
||||
warning: The following rules may cause conflicts when used with the formatter: 'COM812', 'COM819', 'D206', 'E501', 'ISC001', 'Q000', 'Q001', 'Q002', 'Q003', 'W191'. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The following isort options may cause conflicts when used with the formatter: 'isort.force-single-line', 'isort.force-wrap-aliases', 'isort.lines-after-imports', 'isort.lines_between_types'. To avoid unexpected behavior, we recommend disabling these options by removing them from the configuration.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `D206`, `ISC001`, `W191`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
warning: The `flake8-quotes.inline-quotes="single"` option is incompatible with the formatter's `format.quote-style="double"`. We recommend disabling `Q000` and `Q003` when using the formatter, which enforces a consistent quote style. Alternatively, set both options to either `"single"` or `"double"`.
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q001` when using the formatter, which enforces double quotes for multiline strings. Alternatively, set the `flake8-quotes.multiline-quotes` option to `"double"`.`
|
||||
warning: The `flake8-quotes.multiline-quotes="single"` option is incompatible with the formatter. We recommend disabling `Q002` when using the formatter, which enforces double quotes for docstrings. Alternatively, set the `flake8-quotes.docstring-quotes` option to `"double"`.`
|
||||
warning: The isort option `isort.lines-after-imports` with a value other than `-1`, `1` or `2` is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `2`, `1`, or `-1` (default).
|
||||
warning: The isort option `isort.lines-between-types` with a value greater than 1 is incompatible with the formatter. To avoid unexpected behavior, we recommend setting the option to one of: `1` or `0` (default).
|
||||
warning: The isort option `isort.force-wrap-aliases` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.force-wrap-aliases=false` or `format.skip-magic-trailing-comma=false`.
|
||||
warning: The isort option `isort.split-on-trailing-comma` is incompatible with the formatter `format.skip-magic-trailing-comma=true` option. To avoid unexpected behavior, we recommend either setting `isort.split-on-trailing-comma=false` or `format.skip-magic-trailing-comma=false`.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_linter_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
ignore = ["D203", "D212", "COM812", "ISC001"]
|
||||
|
||||
[lint.isort]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
docstring-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
|
||||
[format]
|
||||
skip-magic-trailing-comma = false
|
||||
quote-style = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_rules_default_options() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["ALL"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--no-cache", "--config"])
|
||||
.arg(&ruff_toml)
|
||||
.arg(test_path), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
||||
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
||||
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding then to the `ignore` configuration.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff() {
|
||||
let args = ["format", "--no-cache", "--isolated", "--diff"];
|
||||
@@ -452,8 +580,8 @@ fn test_diff() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
--- resources/test/fixtures/unformatted.ipynb
|
||||
+++ resources/test/fixtures/unformatted.ipynb
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 1
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 1
|
||||
@@ -1,3 +1,4 @@
|
||||
import numpy
|
||||
-maths = (numpy.arange(100)**2).sum()
|
||||
@@ -461,6 +589,30 @@ fn test_diff() {
|
||||
+
|
||||
+maths = (numpy.arange(100) ** 2).sum()
|
||||
+stats = numpy.asarray([1, 2, 3, 4]).median()
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 3
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 3
|
||||
@@ -1,4 +1,6 @@
|
||||
# A cell with IPython escape command
|
||||
def some_function(foo, bar):
|
||||
pass
|
||||
+
|
||||
+
|
||||
%matplotlib inline
|
||||
--- resources/test/fixtures/unformatted.ipynb:cell 4
|
||||
+++ resources/test/fixtures/unformatted.ipynb:cell 4
|
||||
@@ -1,5 +1,10 @@
|
||||
foo = %pwd
|
||||
-def some_function(foo,bar,):
|
||||
+
|
||||
+
|
||||
+def some_function(
|
||||
+ foo,
|
||||
+ bar,
|
||||
+):
|
||||
# Another cell with IPython escape command
|
||||
foo = %pwd
|
||||
print(foo)
|
||||
|
||||
--- resources/test/fixtures/unformatted.py
|
||||
+++ resources/test/fixtures/unformatted.py
|
||||
@@ -1,3 +1,3 @@
|
||||
@@ -469,6 +621,7 @@ fn test_diff() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
2 files would be reformatted, 1 file left unchanged
|
||||
"###);
|
||||
@@ -498,6 +651,7 @@ fn test_diff_no_change() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
1 file would be reformatted
|
||||
"###
|
||||
@@ -531,6 +685,7 @@ fn test_diff_stdin_unformatted() {
|
||||
+y = 2
|
||||
z = 3
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
|
||||
use ruff_python_formatter::{
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions,
|
||||
};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||
|
||||
@@ -871,9 +871,7 @@ struct BlackOptions {
|
||||
line_length: NonZeroU16,
|
||||
#[serde(alias = "skip-magic-trailing-comma")]
|
||||
skip_magic_trailing_comma: bool,
|
||||
#[allow(unused)]
|
||||
#[serde(alias = "force-exclude")]
|
||||
force_exclude: Option<String>,
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
impl Default for BlackOptions {
|
||||
@@ -881,7 +879,7 @@ impl Default for BlackOptions {
|
||||
Self {
|
||||
line_length: NonZeroU16::new(88).unwrap(),
|
||||
skip_magic_trailing_comma: false,
|
||||
force_exclude: None,
|
||||
preview: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,6 +927,11 @@ impl BlackOptions {
|
||||
} else {
|
||||
MagicTrailingComma::Respect
|
||||
})
|
||||
.with_preview(if self.preview {
|
||||
PreviewMode::Enabled
|
||||
} else {
|
||||
PreviewMode::Disabled
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,14 +22,16 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
for rule in rules {
|
||||
let fix_token = match rule.fixable() {
|
||||
FixAvailability::Always | FixAvailability::Sometimes => {
|
||||
format!("<span style='opacity: 1'>{FIX_SYMBOL}</span>")
|
||||
format!("<span title='Automatic fix available'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::None => {
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{FIX_SYMBOL}</span>")
|
||||
}
|
||||
FixAvailability::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>")
|
||||
format!("<span title='Rule is in preview'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
|
||||
format!("<span style='opacity: 0.1' aria-hidden='true'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -79,3 +79,6 @@ from ZeroDivisionError
|
||||
raise IndexError() from ZeroDivisionError
|
||||
|
||||
raise IndexError();
|
||||
|
||||
# RSE102
|
||||
raise Foo()
|
||||
|
||||
@@ -49,6 +49,13 @@ class Apples:
|
||||
def __doc__(self):
|
||||
return "Docstring"
|
||||
|
||||
# Added in Python 3.12
|
||||
def __buffer__(self):
|
||||
return memoryview(b'')
|
||||
|
||||
def __release_buffer__(self, buf):
|
||||
pass
|
||||
|
||||
# Allow dunder methods recommended by attrs.
|
||||
def __attrs_post_init__(self):
|
||||
pass
|
||||
|
||||
@@ -131,7 +131,7 @@ pub(crate) fn check_noqa(
|
||||
|| settings
|
||||
.external
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(code))
|
||||
.any(|external| code.starts_with(external))
|
||||
{
|
||||
valid_codes.push(code);
|
||||
} else {
|
||||
|
||||
@@ -110,18 +110,27 @@ pub(crate) fn implicit(
|
||||
{
|
||||
let (a_range, b_range) = match (a_tok, b_tok) {
|
||||
(Tok::String { .. }, Tok::String { .. }) => (*a_range, *b_range),
|
||||
(Tok::String { .. }, Tok::FStringStart) => (
|
||||
*a_range,
|
||||
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
|
||||
),
|
||||
(Tok::FStringEnd, Tok::String { .. }) => (
|
||||
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
|
||||
*b_range,
|
||||
),
|
||||
(Tok::FStringEnd, Tok::FStringStart) => (
|
||||
indexer.fstring_ranges().innermost(a_range.start()).unwrap(),
|
||||
indexer.fstring_ranges().innermost(b_range.start()).unwrap(),
|
||||
),
|
||||
(Tok::String { .. }, Tok::FStringStart) => {
|
||||
match indexer.fstring_ranges().innermost(b_range.start()) {
|
||||
Some(b_range) => (*a_range, b_range),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
(Tok::FStringEnd, Tok::String { .. }) => {
|
||||
match indexer.fstring_ranges().innermost(a_range.start()) {
|
||||
Some(a_range) => (a_range, *b_range),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
(Tok::FStringEnd, Tok::FStringStart) => {
|
||||
match (
|
||||
indexer.fstring_ranges().innermost(a_range.start()),
|
||||
indexer.fstring_ranges().innermost(b_range.start()),
|
||||
) {
|
||||
(Some(a_range), Some(b_range)) => (a_range, b_range),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ use crate::settings::LinterSettings;
|
||||
/// ```python
|
||||
/// foo = "bar's"
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter automatically removes unnecessary escapes, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct AvoidableEscapedQuote;
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ use super::super::settings::Quote;
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.inline-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent quotes for inline strings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesInlineString {
|
||||
preferred_quote: Quote,
|
||||
@@ -81,6 +88,13 @@ impl AlwaysFixableViolation for BadQuotesInlineString {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.multiline-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces double quotes for multiline strings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesMultilineString {
|
||||
preferred_quote: Quote,
|
||||
@@ -129,6 +143,13 @@ impl AlwaysFixableViolation for BadQuotesMultilineString {
|
||||
///
|
||||
/// ## Options
|
||||
/// - `flake8-quotes.docstring-quotes`
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces double quotes for docstrings, making the rule
|
||||
/// redundant.
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct BadQuotesDocstring {
|
||||
preferred_quote: Quote,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::BindingKind;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -15,6 +16,16 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// Removing the parentheses makes the code more concise.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Parentheses can only be omitted if the exception is a class, as opposed to
|
||||
/// a function call. This rule isn't always capable of distinguishing between
|
||||
/// the two.
|
||||
///
|
||||
/// For example, if you import a function `module.get_exception` from another
|
||||
/// module, and `module.get_exception` returns an exception object, this rule will
|
||||
/// incorrectly mark the parentheses in `raise module.get_exception()` as
|
||||
/// unnecessary.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// raise TypeError()
|
||||
@@ -54,25 +65,32 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
|
||||
|
||||
if arguments.is_empty() {
|
||||
// `raise func()` still requires parentheses; only `raise Class()` does not.
|
||||
if checker
|
||||
.semantic()
|
||||
.lookup_attribute(func)
|
||||
.is_some_and(|id| checker.semantic().binding(id).kind.is_function_definition())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let exception_type = if let Some(id) = checker.semantic().lookup_attribute(func) {
|
||||
match checker.semantic().binding(id).kind {
|
||||
BindingKind::FunctionDefinition(_) => return,
|
||||
BindingKind::ClassDefinition(_) => Some(ExceptionType::Class),
|
||||
BindingKind::Builtin => Some(ExceptionType::Builtin),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
|
||||
// we might as well get it right.
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
|
||||
if exception_type
|
||||
.as_ref()
|
||||
.is_some_and(ExceptionType::is_builtin)
|
||||
&& checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["ctypes", "WinError"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, arguments.range());
|
||||
|
||||
// If the arguments are immediately followed by a `from`, insert whitespace to avoid
|
||||
// a syntax error, as in:
|
||||
// ```python
|
||||
@@ -85,13 +103,25 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
|
||||
.next()
|
||||
.is_some_and(char::is_alphanumeric)
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
" ".to_string(),
|
||||
arguments.range(),
|
||||
)));
|
||||
diagnostic.set_fix(if exception_type.is_some() {
|
||||
Fix::safe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
|
||||
} else {
|
||||
Fix::unsafe_edit(Edit::range_replacement(" ".to_string(), arguments.range()))
|
||||
});
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(arguments.range())));
|
||||
diagnostic.set_fix(if exception_type.is_some() {
|
||||
Fix::safe_edit(Edit::range_deletion(arguments.range()))
|
||||
} else {
|
||||
Fix::unsafe_edit(Edit::range_deletion(arguments.range()))
|
||||
});
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, is_macro::Is)]
|
||||
enum ExceptionType {
|
||||
Class,
|
||||
Builtin,
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ RSE102.py:79:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
79 |+raise IndexError from ZeroDivisionError
|
||||
80 80 |
|
||||
81 81 | raise IndexError();
|
||||
82 82 |
|
||||
|
||||
RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
|
|
||||
@@ -245,6 +246,8 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
80 |
|
||||
81 | raise IndexError();
|
||||
| ^^ RSE102
|
||||
82 |
|
||||
83 | # RSE102
|
||||
|
|
||||
= help: Remove unnecessary parentheses
|
||||
|
||||
@@ -254,5 +257,23 @@ RSE102.py:81:17: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
80 80 |
|
||||
81 |-raise IndexError();
|
||||
81 |+raise IndexError;
|
||||
82 82 |
|
||||
83 83 | # RSE102
|
||||
84 84 | raise Foo()
|
||||
|
||||
RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
|
|
||||
83 | # RSE102
|
||||
84 | raise Foo()
|
||||
| ^^ RSE102
|
||||
|
|
||||
= help: Remove unnecessary parentheses
|
||||
|
||||
ℹ Suggested fix
|
||||
81 81 | raise IndexError();
|
||||
82 82 |
|
||||
83 83 | # RSE102
|
||||
84 |-raise Foo()
|
||||
84 |+raise Foo
|
||||
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ pub(crate) fn organize_imports(
|
||||
block,
|
||||
comments,
|
||||
locator,
|
||||
settings.pycodestyle.max_line_length,
|
||||
settings.line_length,
|
||||
LineWidthBuilder::new(settings.tab_size).add_str(indentation),
|
||||
stylist,
|
||||
&settings.src,
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::Tok;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::fix::edits::pad_start;
|
||||
|
||||
@@ -25,23 +25,51 @@ use crate::fix::edits::pad_start;
|
||||
/// regex = r"\.png$"
|
||||
/// ```
|
||||
///
|
||||
/// Or, if the string already contains a valid escape sequence:
|
||||
/// ```python
|
||||
/// value = "new line\nand invalid escape \_ here"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// value = "new line\nand invalid escape \\_ here"
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals)
|
||||
#[violation]
|
||||
pub struct InvalidEscapeSequence(char);
|
||||
pub struct InvalidEscapeSequence {
|
||||
ch: char,
|
||||
fix_title: FixTitle,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for InvalidEscapeSequence {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let InvalidEscapeSequence(char) = self;
|
||||
format!("Invalid escape sequence: `\\{char}`")
|
||||
let InvalidEscapeSequence { ch, .. } = self;
|
||||
format!("Invalid escape sequence: `\\{ch}`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add backslash to escape sequence".to_string()
|
||||
match self.fix_title {
|
||||
FixTitle::AddBackslash => format!("Add backslash to escape sequence"),
|
||||
FixTitle::UseRawStringLiteral => format!("Use a raw string literal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum FixTitle {
|
||||
AddBackslash,
|
||||
UseRawStringLiteral,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InvalidEscapeChar {
|
||||
ch: char,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
/// W605
|
||||
pub(crate) fn invalid_escape_sequence(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
@@ -50,24 +78,27 @@ pub(crate) fn invalid_escape_sequence(
|
||||
token: &Tok,
|
||||
token_range: TextRange,
|
||||
) {
|
||||
let token_source_code = match token {
|
||||
let (token_source_code, string_start_location) = match token {
|
||||
Tok::FStringMiddle { value, is_raw } => {
|
||||
if *is_raw {
|
||||
return;
|
||||
}
|
||||
value.as_str()
|
||||
let Some(range) = indexer.fstring_ranges().innermost(token_range.start()) else {
|
||||
return;
|
||||
};
|
||||
(value.as_str(), range.start())
|
||||
}
|
||||
Tok::String { kind, .. } => {
|
||||
if kind.is_raw() {
|
||||
return;
|
||||
}
|
||||
locator.slice(token_range)
|
||||
(locator.slice(token_range), token_range.start())
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let mut contains_valid_escape_sequence = false;
|
||||
let mut invalid_escape_sequence = Vec::new();
|
||||
let mut invalid_escape_chars = Vec::new();
|
||||
|
||||
let mut prev = None;
|
||||
let bytes = token_source_code.as_bytes();
|
||||
@@ -154,38 +185,49 @@ pub(crate) fn invalid_escape_sequence(
|
||||
|
||||
let location = token_range.start() + TextSize::try_from(i).unwrap();
|
||||
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
|
||||
invalid_escape_sequence.push(Diagnostic::new(InvalidEscapeSequence(next_char), range));
|
||||
invalid_escape_chars.push(InvalidEscapeChar {
|
||||
ch: next_char,
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
let mut invalid_escape_sequence = Vec::new();
|
||||
if contains_valid_escape_sequence {
|
||||
// Escape with backslash.
|
||||
for diagnostic in &mut invalid_escape_sequence {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
for invalid_escape_char in &invalid_escape_chars {
|
||||
let diagnostic = Diagnostic::new(
|
||||
InvalidEscapeSequence {
|
||||
ch: invalid_escape_char.ch,
|
||||
fix_title: FixTitle::AddBackslash,
|
||||
},
|
||||
invalid_escape_char.range,
|
||||
)
|
||||
.with_fix(Fix::safe_edit(Edit::insertion(
|
||||
r"\".to_string(),
|
||||
diagnostic.start() + TextSize::from(1),
|
||||
invalid_escape_char.range.start() + TextSize::from(1),
|
||||
)));
|
||||
invalid_escape_sequence.push(diagnostic);
|
||||
}
|
||||
} else {
|
||||
let tok_start = if token.is_f_string_middle() {
|
||||
// SAFETY: If this is a `FStringMiddle` token, then the indexer
|
||||
// must have the f-string range.
|
||||
indexer
|
||||
.fstring_ranges()
|
||||
.innermost(token_range.start())
|
||||
.unwrap()
|
||||
.start()
|
||||
} else {
|
||||
token_range.start()
|
||||
};
|
||||
// Turn into raw string.
|
||||
for diagnostic in &mut invalid_escape_sequence {
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||
// `returnr"foo"` is not.
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
pad_start("r".to_string(), tok_start, locator),
|
||||
tok_start,
|
||||
)));
|
||||
for invalid_escape_char in &invalid_escape_chars {
|
||||
let diagnostic = Diagnostic::new(
|
||||
InvalidEscapeSequence {
|
||||
ch: invalid_escape_char.ch,
|
||||
fix_title: FixTitle::UseRawStringLiteral,
|
||||
},
|
||||
invalid_escape_char.range,
|
||||
)
|
||||
.with_fix(
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`,
|
||||
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
|
||||
// `returnr"foo"` is not.
|
||||
Fix::safe_edit(Edit::insertion(
|
||||
pad_start("r".to_string(), string_start_location, locator),
|
||||
string_start_location,
|
||||
)),
|
||||
);
|
||||
invalid_escape_sequence.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,16 @@ use super::LogicalLine;
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `indent-width` with a value other than `4`.
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
|
||||
/// [formatter]:https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct IndentationWithInvalidMultiple {
|
||||
indent_size: usize,
|
||||
@@ -55,7 +64,15 @@ impl Violation for IndentationWithInvalidMultiple {
|
||||
/// # a = 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `indent-width` with a value other than `4`.
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
|
||||
/// [formatter]:https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct IndentationWithInvalidMultipleComment {
|
||||
indent_size: usize,
|
||||
|
||||
@@ -26,7 +26,15 @@ use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
/// a = 1
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `format.indent-style="tab"`.
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct TabIndentation;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.`
|
||||
3 |
|
||||
4 | #: W605:2:1
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
1 1 | #: W605:1:10
|
||||
@@ -27,7 +27,7 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.`
|
||||
| ^^ W605
|
||||
7 | '''
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
2 2 | regex = '\.png$'
|
||||
@@ -47,7 +47,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_`
|
||||
| ^^ W605
|
||||
12 | )
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
8 8 |
|
||||
@@ -68,7 +68,7 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_`
|
||||
19 | in the middle
|
||||
20 | """
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
12 12 | )
|
||||
@@ -107,7 +107,7 @@ W605_0.py:28:12: W605 [*] Invalid escape sequence: `\.`
|
||||
29 |
|
||||
30 | #: Okay
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
25 25 |
|
||||
|
||||
@@ -9,7 +9,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.`
|
||||
3 |
|
||||
4 | #: W605:2:1
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
1 1 | #: W605:1:10
|
||||
@@ -27,7 +27,7 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.`
|
||||
| ^^ W605
|
||||
7 | '''
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
2 2 | regex = '\.png$'
|
||||
@@ -47,7 +47,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_`
|
||||
| ^^ W605
|
||||
12 | )
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
8 8 |
|
||||
@@ -68,7 +68,7 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_`
|
||||
19 | in the middle
|
||||
20 | """
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
12 12 | )
|
||||
@@ -89,7 +89,7 @@ W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.`
|
||||
26 |
|
||||
27 | #: Okay
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
22 22 |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
||||
|
|
||||
@@ -9,7 +9,7 @@ W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
||||
5 |
|
||||
6 | #: W605:2:1
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
1 1 | # Same as `W605_0.py` but using f-strings instead.
|
||||
@@ -29,7 +29,7 @@ W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.`
|
||||
| ^^ W605
|
||||
9 | '''
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
4 4 | regex = f'\.png$'
|
||||
@@ -49,7 +49,7 @@ W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_`
|
||||
| ^^ W605
|
||||
14 | )
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
10 10 |
|
||||
@@ -70,7 +70,7 @@ W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
||||
21 | in the middle
|
||||
22 | """
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
14 14 | )
|
||||
@@ -129,7 +129,7 @@ W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
||||
45 | value = f'\{1}'
|
||||
46 | value = f'{1:\}'
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
41 41 | ''' # noqa
|
||||
@@ -150,7 +150,7 @@ W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
||||
46 | value = f'{1:\}'
|
||||
47 | value = f"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
42 42 |
|
||||
@@ -171,7 +171,7 @@ W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
||||
47 | value = f"{f"\{1}"}"
|
||||
48 | value = rf"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
43 43 | regex = f'\\\_'
|
||||
@@ -191,7 +191,7 @@ W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
||||
| ^^ W605
|
||||
48 | value = rf"{f"\{1}"}"
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
44 44 | value = f'\{{1}}'
|
||||
@@ -212,7 +212,7 @@ W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
||||
49 |
|
||||
50 | # Okay
|
||||
|
|
||||
= help: Add backslash to escape sequence
|
||||
= help: Use a raw string literal
|
||||
|
||||
ℹ Fix
|
||||
45 45 | value = f'\{1}'
|
||||
|
||||
@@ -14,9 +14,7 @@ use crate::registry::Rule;
|
||||
/// Checks for docstrings that are indented with tabs.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// [PEP 8](https://peps.python.org/pep-0008/#tabs-or-spaces) recommends using
|
||||
/// spaces over tabs for indentation.
|
||||
///
|
||||
/// [PEP 8] recommends using spaces over tabs for indentation.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -38,10 +36,20 @@ use crate::registry::Rule;
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `format.indent-style="tab"`.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#tabs-or-spaces
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter
|
||||
#[violation]
|
||||
pub struct IndentWithSpaces;
|
||||
|
||||
@@ -126,12 +134,17 @@ impl AlwaysFixableViolation for UnderIndentation {
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/
|
||||
/// [formatter]:https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct OverIndentation;
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ fn is_known_dunder_method(method: &str) -> bool {
|
||||
| "__attrs_pre_init__"
|
||||
| "__await__"
|
||||
| "__bool__"
|
||||
| "__buffer__"
|
||||
| "__bytes__"
|
||||
| "__call__"
|
||||
| "__ceil__"
|
||||
@@ -166,6 +167,7 @@ fn is_known_dunder_method(method: &str) -> bool {
|
||||
| "__rdivmod__"
|
||||
| "__reduce__"
|
||||
| "__reduce_ex__"
|
||||
| "__release_buffer__"
|
||||
| "__repr__"
|
||||
| "__reversed__"
|
||||
| "__rfloordiv__"
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{checkers::ast::Checker, rules::flake8_unused_arguments::helpers};
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Unused `self` parameters are usually a sign of a method that could be
|
||||
/// replaced by a function or a static method.
|
||||
/// replaced by a function, class method, or static method.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -26,10 +26,8 @@ use crate::{checkers::ast::Checker, rules::flake8_unused_arguments::helpers};
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Person:
|
||||
/// @staticmethod
|
||||
/// def greeting():
|
||||
/// print("Greetings friend!")
|
||||
/// def greeting():
|
||||
/// print("Greetings friend!")
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct NoSelfUse {
|
||||
@@ -40,7 +38,7 @@ impl Violation for NoSelfUse {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NoSelfUse { method_name } = self;
|
||||
format!("Method `{method_name}` could be a function or static method")
|
||||
format!("Method `{method_name}` could be a function, class method, or static method")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
no_self_use.py:7:28: PLR6301 Method `developer_greeting` could be a function or static method
|
||||
no_self_use.py:7:28: PLR6301 Method `developer_greeting` could be a function, class method, or static method
|
||||
|
|
||||
6 | class Person:
|
||||
7 | def developer_greeting(self, name): # [no-self-use]
|
||||
@@ -9,7 +9,7 @@ no_self_use.py:7:28: PLR6301 Method `developer_greeting` could be a function or
|
||||
8 | print(f"Greetings {name}!")
|
||||
|
|
||||
|
||||
no_self_use.py:10:20: PLR6301 Method `greeting_1` could be a function or static method
|
||||
no_self_use.py:10:20: PLR6301 Method `greeting_1` could be a function, class method, or static method
|
||||
|
|
||||
8 | print(f"Greetings {name}!")
|
||||
9 |
|
||||
@@ -18,7 +18,7 @@ no_self_use.py:10:20: PLR6301 Method `greeting_1` could be a function or static
|
||||
11 | print("Hello!")
|
||||
|
|
||||
|
||||
no_self_use.py:13:20: PLR6301 Method `greeting_2` could be a function or static method
|
||||
no_self_use.py:13:20: PLR6301 Method `greeting_2` could be a function, class method, or static method
|
||||
|
|
||||
11 | print("Hello!")
|
||||
12 |
|
||||
|
||||
@@ -17,7 +17,7 @@ mod tests {
|
||||
use crate::pyproject_toml::lint_pyproject_toml;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::resolve_per_file_ignores;
|
||||
use crate::settings::types::{CodePattern, PerFileIgnore, PythonVersion};
|
||||
use crate::settings::types::{PerFileIgnore, PythonVersion};
|
||||
use crate::test::{test_path, test_resource_path};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_0.py"),
|
||||
&settings::LinterSettings {
|
||||
external: vec![CodePattern::new("V101")?],
|
||||
external: vec!["V101".to_string()],
|
||||
..settings::LinterSettings::for_rules(vec![
|
||||
Rule::UnusedNOQA,
|
||||
Rule::LineTooLong,
|
||||
@@ -121,11 +121,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruf100_0_glob() -> Result<()> {
|
||||
fn ruf100_0_prefix() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("ruff/RUF100_0.py"),
|
||||
&settings::LinterSettings {
|
||||
external: vec![CodePattern::new("V*")?],
|
||||
external: vec!["V".to_string()],
|
||||
..settings::LinterSettings::for_rules(vec
|
||||
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
|
||||
#[violation]
|
||||
pub struct UnusedNOQA {
|
||||
pub codes: Option<UnusedCodes>,
|
||||
|
||||
@@ -14,6 +14,7 @@ use rustc_hash::FxHashSet;
|
||||
use crate::codes::RuleCodePrefix;
|
||||
use ruff_macros::CacheKey;
|
||||
|
||||
use crate::line_width::LineLength;
|
||||
use crate::registry::{Linter, Rule, RuleSet};
|
||||
use crate::rules::{
|
||||
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
|
||||
@@ -28,7 +29,7 @@ use crate::{codes, RuleSelector};
|
||||
use super::line_width::IndentWidth;
|
||||
|
||||
use self::rule_table::RuleTable;
|
||||
use self::types::{CodePattern, PreviewMode};
|
||||
use self::types::PreviewMode;
|
||||
use crate::rule_selector::PreviewOptions;
|
||||
|
||||
pub mod flags;
|
||||
@@ -53,12 +54,13 @@ pub struct LinterSettings {
|
||||
pub allowed_confusables: FxHashSet<char>,
|
||||
pub builtins: Vec<String>,
|
||||
pub dummy_variable_rgx: Regex,
|
||||
pub external: Vec<CodePattern>,
|
||||
pub external: Vec<String>,
|
||||
pub ignore_init_module_imports: bool,
|
||||
pub logger_objects: Vec<String>,
|
||||
pub namespace_packages: Vec<PathBuf>,
|
||||
pub src: Vec<PathBuf>,
|
||||
pub tab_size: IndentWidth,
|
||||
pub line_length: LineLength,
|
||||
pub task_tags: Vec<String>,
|
||||
pub typing_modules: Vec<String>,
|
||||
|
||||
@@ -155,6 +157,7 @@ impl LinterSettings {
|
||||
src: vec![path_dedot::CWD.clone()],
|
||||
// Needs duplicating
|
||||
tab_size: IndentWidth::default(),
|
||||
line_length: LineLength::default(),
|
||||
|
||||
task_tags: TASK_TAGS.iter().map(ToString::to_string).collect(),
|
||||
typing_modules: vec![],
|
||||
|
||||
@@ -338,18 +338,9 @@ impl Deref for Version {
|
||||
///
|
||||
/// [`glob::Pattern`] matches a little differently than we ideally want to.
|
||||
/// Specifically it uses `**` to match an arbitrary number of subdirectories,
|
||||
/// however this not relevant since identifiers don't contains slashes.
|
||||
/// luckily this not relevant since identifiers don't contains slashes.
|
||||
///
|
||||
/// For reference pep8-naming uses
|
||||
/// [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for
|
||||
/// pattern matching.
|
||||
pub type IdentifierPattern = glob::Pattern;
|
||||
|
||||
/// Pattern to match a rule code.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// [`glob::Pattern`] matches a little differently than we ideally want to.
|
||||
/// Specifically it uses `**` to match an arbitrary number of subdirectories,
|
||||
/// however this not relevant since identifiers don't contains slashes.
|
||||
pub type CodePattern = glob::Pattern;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::Result;
|
||||
use similar::TextDiff;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -88,7 +88,12 @@ impl SourceKind {
|
||||
}
|
||||
|
||||
/// Write a diff of the transformed source file to `stdout`.
|
||||
pub fn diff(&self, other: &Self, path: Option<&Path>, writer: &mut dyn Write) -> Result<()> {
|
||||
pub fn diff(
|
||||
&self,
|
||||
other: &Self,
|
||||
path: Option<&Path>,
|
||||
writer: &mut dyn Write,
|
||||
) -> io::Result<()> {
|
||||
match (self, other) {
|
||||
(SourceKind::Python(src), SourceKind::Python(dst)) => {
|
||||
let text_diff = TextDiff::from_lines(src, dst);
|
||||
@@ -154,7 +159,7 @@ impl SourceKind {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => bail!("cannot diff Python source code with Jupyter notebook source code"),
|
||||
_ => panic!("cannot diff Python source code with Jupyter notebook source code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,28 +537,6 @@ def update_emission_strength():
|
||||
value = self.emission_strength * 2
|
||||
```
|
||||
|
||||
#### Type annotations may be parenthesized when expanded ([#7315](https://github.com/astral-sh/ruff/issues/7315))
|
||||
|
||||
Black will avoid parenthesizing type annotations in an annotated assignment, while Ruff will insert
|
||||
parentheses in some cases.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
# Black
|
||||
StartElementHandler: Callable[[str, dict[str, str]], Any] | Callable[[str, list[str]], Any] | Callable[
|
||||
[str, dict[str, str], list[str]], Any
|
||||
] | None
|
||||
|
||||
# Ruff
|
||||
StartElementHandler: (
|
||||
Callable[[str, dict[str, str]], Any]
|
||||
| Callable[[str, list[str]], Any]
|
||||
| Callable[[str, dict[str, str], list[str]], Any]
|
||||
| None
|
||||
)
|
||||
```
|
||||
|
||||
#### Call chain calls break differently ([#7051](https://github.com/astral-sh/ruff/issues/7051))
|
||||
|
||||
Black occasionally breaks call chains differently than Ruff; in particular, Black occasionally
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# flags: --pyi
|
||||
from typing import Union
|
||||
|
||||
@bird
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
''''''
|
||||
|
||||
'\''
|
||||
'"'
|
||||
"'"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""""""
|
||||
|
||||
"'"
|
||||
'"'
|
||||
"'"
|
||||
|
||||
16
crates/ruff_python_formatter/resources/test/fixtures/black/raw_docstring.py
vendored
Normal file
16
crates/ruff_python_formatter/resources/test/fixtures/black/raw_docstring.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# flags: --preview --skip-string-normalization
|
||||
class C:
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
def f():
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
class SingleQuotes:
|
||||
|
||||
|
||||
r'''Raw'''
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
14
crates/ruff_python_formatter/resources/test/fixtures/black/raw_docstring.py.expect
vendored
Normal file
14
crates/ruff_python_formatter/resources/test/fixtures/black/raw_docstring.py.expect
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
class C:
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
def f():
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
class SingleQuotes:
|
||||
r'''Raw'''
|
||||
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
10
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_pow_spacing.py
vendored
Normal file
10
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_pow_spacing.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# No spacing
|
||||
5 ** 5
|
||||
5.0 ** 5.0
|
||||
1e5 ** 2e5
|
||||
True ** True
|
||||
False ** False
|
||||
None ** None
|
||||
|
||||
# Space
|
||||
"a" ** "b"
|
||||
@@ -180,3 +180,16 @@ if "root" not in (
|
||||
):
|
||||
msg = "Could not find root. Please try a different forest."
|
||||
raise ValueError(msg)
|
||||
|
||||
# Regression for https://github.com/astral-sh/ruff/issues/8183
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
if True:
|
||||
if False:
|
||||
x: int # Optional[int]
|
||||
@@ -0,0 +1,8 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
x: int # Optional[int]
|
||||
@@ -1,7 +1,39 @@
|
||||
# Below is black stable style
|
||||
# In preview style, black always breaks the right side first
|
||||
"""
|
||||
Black's `Preview.module_docstring_newlines`
|
||||
"""
|
||||
first_stmt_after_module_level_docstring = 1
|
||||
|
||||
if True:
|
||||
|
||||
class CachedRepository:
|
||||
# Black's `Preview.dummy_implementations`
|
||||
def get_release_info(self): ...
|
||||
|
||||
|
||||
def raw_docstring():
|
||||
|
||||
r"""Black's `Preview.accept_raw_docstrings`
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def reference_docstring_newlines():
|
||||
|
||||
"""A regular docstring for comparison
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RemoveNewlineBeforeClassDocstring:
|
||||
|
||||
"""Black's `Preview.no_blank_line_before_class_docstring`"""
|
||||
|
||||
|
||||
def f():
|
||||
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
] = cccccccc.ccccccccccccc.cccccccc
|
||||
@@ -1,6 +1,21 @@
|
||||
# Regression test: Don't forget the parentheses in the value when breaking
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
)
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
)= Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
|
||||
JSONSerializable: TypeAlias = (
|
||||
"str | int | float | bool | None | list | tuple | JSONMapping"
|
||||
)
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {1, 2, 3, 4}
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = aaaaaaaaaaaaaaaa
|
||||
|
||||
# Regression test: Don't forget the parentheses in the annotation when breaking
|
||||
class DefaultRunner:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# comment
|
||||
|
||||
class Test(
|
||||
Aaaaaaaaaaaaaaaaa,
|
||||
Bbbbbbbbbbbbbbbb,
|
||||
|
||||
@@ -14,3 +14,35 @@ return (
|
||||
len(node.parents) for node in self.node_map.values()
|
||||
)
|
||||
)
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8042
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv" +
|
||||
"text/csv" +
|
||||
output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv" + "text/csv",
|
||||
output.getvalue().encode("utf-8----------------")
|
||||
)
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv",
|
||||
"text/csv",
|
||||
output.getvalue().encode("utf-8----------------")
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv" + "text/csv" + output.getvalue().encode("utf-8----------------"),
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv" + "text/csv", output.getvalue().encode("utf-8----------------")
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv", "text/csv", output.getvalue().encode("utf-8----------------")
|
||||
|
||||
@@ -6,12 +6,13 @@ use anyhow::{format_err, Context, Result};
|
||||
use clap::{command, Parser, ValueEnum};
|
||||
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::{parse_ok_tokens, Mode};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::collect_comments;
|
||||
use crate::{format_module_ast, PyFormatOptions};
|
||||
use crate::{format_module_ast, PreviewMode, PyFormatOptions};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum Emit {
|
||||
@@ -23,6 +24,7 @@ pub enum Emit {
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways
|
||||
pub struct Cli {
|
||||
/// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported
|
||||
pub files: Vec<PathBuf>,
|
||||
@@ -33,20 +35,27 @@ pub struct Cli {
|
||||
#[clap(long)]
|
||||
pub check: bool,
|
||||
#[clap(long)]
|
||||
pub preview: bool,
|
||||
#[clap(long)]
|
||||
pub print_ir: bool,
|
||||
#[clap(long)]
|
||||
pub print_comments: bool,
|
||||
}
|
||||
|
||||
pub fn format_and_debug_print(source: &str, cli: &Cli, source_type: &Path) -> Result<String> {
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source)
|
||||
pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Result<String> {
|
||||
let source_type = PySourceType::from(source_path);
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type)
|
||||
.map_err(|err| format_err!("Source contains syntax errors {err:?}"))?;
|
||||
|
||||
// Parse the AST.
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")
|
||||
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")
|
||||
.context("Syntax error in input")?;
|
||||
|
||||
let options = PyFormatOptions::from_extension(source_type);
|
||||
let options = PyFormatOptions::from_extension(source_path).with_preview(if cli.preview {
|
||||
PreviewMode::Enabled
|
||||
} else {
|
||||
PreviewMode::Disabled
|
||||
});
|
||||
|
||||
let source_code = SourceCode::new(source);
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)
|
||||
|
||||
@@ -507,13 +507,12 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// def func():
|
||||
/// class Class:
|
||||
/// ...
|
||||
/// # comment
|
||||
/// ```
|
||||
///
|
||||
/// This builder will insert two empty lines before the comment.
|
||||
/// ```
|
||||
pub(crate) fn empty_lines_before_trailing_comments<'a>(
|
||||
f: &PyFormatter,
|
||||
comments: &'a [SourceComment],
|
||||
@@ -555,3 +554,69 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the empty lines between a node and its leading comments.
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// # comment
|
||||
///
|
||||
/// class Class:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// While `leading_comments` will preserve the existing empty line, this builder will insert an
|
||||
/// additional empty line before the comment.
|
||||
pub(crate) fn empty_lines_after_leading_comments<'a>(
|
||||
f: &PyFormatter,
|
||||
comments: &'a [SourceComment],
|
||||
) -> FormatEmptyLinesAfterLeadingComments<'a> {
|
||||
// Black has different rules for stub vs. non-stub and top level vs. indented
|
||||
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
|
||||
(PySourceType::Stub, NodeLevel::TopLevel) => 1,
|
||||
(PySourceType::Stub, _) => 0,
|
||||
(_, NodeLevel::TopLevel) => 2,
|
||||
(_, _) => 1,
|
||||
};
|
||||
|
||||
FormatEmptyLinesAfterLeadingComments {
|
||||
comments,
|
||||
empty_lines,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct FormatEmptyLinesAfterLeadingComments<'a> {
|
||||
/// The leading comments of the node.
|
||||
comments: &'a [SourceComment],
|
||||
/// The expected number of empty lines after the leading comments.
|
||||
empty_lines: u32,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatEmptyLinesAfterLeadingComments<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
|
||||
if let Some(comment) = self
|
||||
.comments
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
let actual = lines_after(comment.end(), f.context().source()).saturating_sub(1);
|
||||
// If there are no empty lines, keep the comment tight to the node.
|
||||
if actual == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If there are more than enough empty lines already, `leading_comments` will
|
||||
// trim them as necessary.
|
||||
if actual >= self.empty_lines {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for _ in actual..self.empty_lines {
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,10 +548,10 @@ mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::Mod;
|
||||
use ruff_python_ast::{Mod, PySourceType};
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
|
||||
use ruff_python_parser::{parse_ok_tokens, Mode};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
|
||||
use crate::comments::Comments;
|
||||
@@ -565,9 +565,10 @@ mod tests {
|
||||
impl<'a> CommentsTestCase<'a> {
|
||||
fn from_code(source: &'a str) -> Self {
|
||||
let source_code = SourceCode::new(source);
|
||||
let source_type = PySourceType::Python;
|
||||
let (tokens, comment_ranges) =
|
||||
tokens_and_ranges(source).expect("Expect source to be valid Python");
|
||||
let parsed = parse_ok_tokens(tokens, source, Mode::Module, "test.py")
|
||||
tokens_and_ranges(source, source_type).expect("Expect source to be valid Python");
|
||||
let parsed = parse_ok_tokens(tokens, source, source_type.as_mode(), "test.py")
|
||||
.expect("Expect source to be valid Python");
|
||||
|
||||
CommentsTestCase {
|
||||
|
||||
@@ -506,7 +506,12 @@ const fn is_simple_power_operand(expr: &Expr) -> bool {
|
||||
op: UnaryOp::Not, ..
|
||||
}) => false,
|
||||
Expr::Constant(ExprConstant {
|
||||
value: Constant::Complex { .. } | Constant::Float(_) | Constant::Int(_),
|
||||
value:
|
||||
Constant::Complex { .. }
|
||||
| Constant::Float(_)
|
||||
| Constant::Int(_)
|
||||
| Constant::None
|
||||
| Constant::Bool(_),
|
||||
..
|
||||
}) => true,
|
||||
Expr::Name(_) => true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_python_ast::ExprIpyEscapeCommand;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -11,3 +12,13 @@ impl FormatNodeRule<ExprIpyEscapeCommand> for FormatExprIpyEscapeCommand {
|
||||
source_text_slice(item.range()).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedsParentheses for ExprIpyEscapeCommand {
|
||||
fn needs_parentheses(
|
||||
&self,
|
||||
_parent: ruff_python_ast::AnyNodeRef,
|
||||
_context: &PyFormatContext,
|
||||
) -> OptionalParentheses {
|
||||
OptionalParentheses::Never
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,10 +172,13 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
|
||||
.finish()
|
||||
}
|
||||
TupleParentheses::Preserve => group(&ExprSequence::new(item)).fmt(f),
|
||||
TupleParentheses::NeverPreserve | TupleParentheses::OptionalParentheses => {
|
||||
TupleParentheses::NeverPreserve => {
|
||||
optional_parentheses(&ExprSequence::new(item)).fmt(f)
|
||||
}
|
||||
TupleParentheses::Default => {
|
||||
TupleParentheses::OptionalParentheses if item.elts.len() == 2 => {
|
||||
optional_parentheses(&ExprSequence::new(item)).fmt(f)
|
||||
}
|
||||
TupleParentheses::Default | TupleParentheses::OptionalParentheses => {
|
||||
parenthesize_if_expands(&ExprSequence::new(item)).fmt(f)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -476,7 +476,7 @@ impl NeedsParentheses for Expr {
|
||||
Expr::List(expr) => expr.needs_parentheses(parent, context),
|
||||
Expr::Tuple(expr) => expr.needs_parentheses(parent, context),
|
||||
Expr::Slice(expr) => expr.needs_parentheses(parent, context),
|
||||
Expr::IpyEscapeCommand(_) => todo!(),
|
||||
Expr::IpyEscapeCommand(expr) => expr.needs_parentheses(parent, context),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,16 +526,20 @@ fn can_omit_optional_parentheses(expr: &Expr, context: &PyFormatContext) -> bool
|
||||
&& has_parentheses(expr, context).is_some_and(OwnParentheses::is_non_empty)
|
||||
}
|
||||
|
||||
// Only use the layout if the first or last expression has parentheses of some sort, and
|
||||
// Only use the layout if the first expression starts with parentheses
|
||||
// or the last expression ends with parentheses of some sort, and
|
||||
// those parentheses are non-empty.
|
||||
let first_parenthesized = visitor
|
||||
.first
|
||||
.is_some_and(|first| is_parenthesized(first, context));
|
||||
let last_parenthesized = visitor
|
||||
if visitor
|
||||
.last
|
||||
.is_some_and(|last| is_parenthesized(last, context));
|
||||
|
||||
first_parenthesized || last_parenthesized
|
||||
.is_some_and(|last| is_parenthesized(last, context))
|
||||
{
|
||||
true
|
||||
} else {
|
||||
visitor
|
||||
.first
|
||||
.expression()
|
||||
.is_some_and(|first| is_parenthesized(first, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,7 +549,7 @@ struct CanOmitOptionalParenthesesVisitor<'input> {
|
||||
max_precedence_count: u32,
|
||||
any_parenthesized_expressions: bool,
|
||||
last: Option<&'input Expr>,
|
||||
first: Option<&'input Expr>,
|
||||
first: First<'input>,
|
||||
context: &'input PyFormatContext<'input>,
|
||||
}
|
||||
|
||||
@@ -557,7 +561,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
|
||||
max_precedence_count: 0,
|
||||
any_parenthesized_expressions: false,
|
||||
last: None,
|
||||
first: None,
|
||||
first: First::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,6 +674,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
|
||||
if op.is_invert() {
|
||||
self.update_max_precedence(OperatorPrecedence::BitwiseInversion);
|
||||
}
|
||||
self.first.set_if_none(First::Token);
|
||||
}
|
||||
|
||||
// `[a, b].test.test[300].dot`
|
||||
@@ -706,20 +711,25 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
|
||||
self.update_max_precedence(OperatorPrecedence::String);
|
||||
}
|
||||
|
||||
Expr::Tuple(_)
|
||||
| Expr::NamedExpr(_)
|
||||
| Expr::GeneratorExp(_)
|
||||
| Expr::Lambda(_)
|
||||
// Expressions with sub expressions but a preceding token
|
||||
// Mark this expression as first expression and not the sub expression.
|
||||
Expr::Lambda(_)
|
||||
| Expr::Await(_)
|
||||
| Expr::Yield(_)
|
||||
| Expr::YieldFrom(_)
|
||||
| Expr::Starred(_) => {
|
||||
self.first.set_if_none(First::Token);
|
||||
}
|
||||
|
||||
Expr::Tuple(_)
|
||||
| Expr::NamedExpr(_)
|
||||
| Expr::GeneratorExp(_)
|
||||
| Expr::FormattedValue(_)
|
||||
| Expr::FString(_)
|
||||
| Expr::Constant(_)
|
||||
| Expr::Starred(_)
|
||||
| Expr::Name(_)
|
||||
| Expr::Slice(_) => {}
|
||||
Expr::IpyEscapeCommand(_) => todo!(),
|
||||
| Expr::Slice(_)
|
||||
| Expr::IpyEscapeCommand(_) => {}
|
||||
};
|
||||
|
||||
walk_expr(self, expr);
|
||||
@@ -741,8 +751,32 @@ impl<'input> PreorderVisitor<'input> for CanOmitOptionalParenthesesVisitor<'inpu
|
||||
self.visit_subexpression(expr);
|
||||
}
|
||||
|
||||
if self.first.is_none() {
|
||||
self.first = Some(expr);
|
||||
self.first.set_if_none(First::Expression(expr));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum First<'a> {
|
||||
None,
|
||||
|
||||
/// Expression starts with a non-parentheses token. E.g. `not a`
|
||||
Token,
|
||||
|
||||
Expression(&'a Expr),
|
||||
}
|
||||
|
||||
impl<'a> First<'a> {
|
||||
#[inline]
|
||||
fn set_if_none(&mut self, first: First<'a>) {
|
||||
if matches!(self, First::None) {
|
||||
*self = first;
|
||||
}
|
||||
}
|
||||
|
||||
fn expression(self) -> Option<&'a Expr> {
|
||||
match self {
|
||||
First::None | First::Token => None,
|
||||
First::Expression(expr) => Some(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use ruff_python_ast::AstNode;
|
||||
use ruff_python_ast::Mod;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
use ruff_python_parser::lexer::LexicalError;
|
||||
use ruff_python_parser::{parse_ok_tokens, Mode, ParseError};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode, ParseError};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
@@ -130,8 +130,9 @@ pub fn format_module_source(
|
||||
source: &str,
|
||||
options: PyFormatOptions,
|
||||
) -> Result<Printed, FormatModuleError> {
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source)?;
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, "<filename>")?;
|
||||
let source_type = options.source_type();
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type)?;
|
||||
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")?;
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options)?;
|
||||
Ok(formatted.print()?)
|
||||
}
|
||||
@@ -172,9 +173,10 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_index::tokens_and_ranges;
|
||||
|
||||
use ruff_python_parser::{parse_ok_tokens, Mode};
|
||||
use ruff_python_parser::{parse_ok_tokens, AsMode};
|
||||
|
||||
use crate::{format_module_ast, format_module_source, PyFormatOptions};
|
||||
|
||||
@@ -213,11 +215,12 @@ def main() -> None:
|
||||
]
|
||||
|
||||
"#;
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source).unwrap();
|
||||
let source_type = PySourceType::Python;
|
||||
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type).unwrap();
|
||||
|
||||
// Parse the AST.
|
||||
let source_path = "code_inline.py";
|
||||
let module = parse_ok_tokens(tokens, source, Mode::Module, source_path).unwrap();
|
||||
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), source_path).unwrap();
|
||||
let options = PyFormatOptions::from_extension(Path::new(source_path));
|
||||
let formatted = format_module_ast(&module, &comment_ranges, source, options).unwrap();
|
||||
|
||||
|
||||
@@ -250,6 +250,10 @@ impl MagicTrailingComma {
|
||||
pub const fn is_respect(self) -> bool {
|
||||
matches!(self, Self::Respect)
|
||||
}
|
||||
|
||||
pub const fn is_ignore(self) -> bool {
|
||||
matches!(self, Self::Ignore)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MagicTrailingComma {
|
||||
|
||||
@@ -391,7 +391,9 @@ pub(crate) fn clause_body<'a>(
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
if f.options().source_type().is_stub()
|
||||
// In stable, stubs are only collapsed in stub files, in preview this is consistently
|
||||
// applied everywhere
|
||||
if (f.options().source_type().is_stub() || f.options().preview().is_enabled())
|
||||
&& contains_only_an_ellipsis(self.body, f.context().comments())
|
||||
&& self.trailing_comments.is_empty()
|
||||
{
|
||||
|
||||
@@ -21,12 +21,7 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
target.format(),
|
||||
token(":"),
|
||||
space(),
|
||||
maybe_parenthesize_expression(annotation, item, Parenthesize::IfBreaks)
|
||||
]
|
||||
[target.format(), token(":"), space(), annotation.format(),]
|
||||
)?;
|
||||
|
||||
if let Some(value) = value {
|
||||
|
||||
@@ -3,7 +3,9 @@ use ruff_python_ast::{Decorator, StmtClassDef};
|
||||
use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use crate::comments::format::{
|
||||
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
|
||||
};
|
||||
use crate::comments::{leading_comments, trailing_comments, SourceComment};
|
||||
use crate::prelude::*;
|
||||
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
||||
@@ -32,6 +34,29 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
|
||||
let (leading_definition_comments, trailing_definition_comments) =
|
||||
dangling_comments.split_at(trailing_definition_comments_start);
|
||||
|
||||
// If the class contains leading comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// # comment
|
||||
//
|
||||
// class Class:
|
||||
// ...
|
||||
// ```
|
||||
//
|
||||
// At the top-level in a non-stub file, reformat as:
|
||||
// ```python
|
||||
// # comment
|
||||
//
|
||||
//
|
||||
// class Class:
|
||||
// ...
|
||||
// ```
|
||||
// Note that this is only really relevant for the specific case in which there's a single
|
||||
// newline between the comment and the node, but we _require_ two newlines. If there are
|
||||
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
|
||||
// are more than two, then `leading_comments` will preserve the correct number of newlines.
|
||||
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtFunctionDef;
|
||||
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use crate::comments::format::{
|
||||
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
|
||||
};
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
||||
@@ -30,6 +32,29 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
|
||||
let (leading_definition_comments, trailing_definition_comments) =
|
||||
dangling_comments.split_at(trailing_definition_comments_start);
|
||||
|
||||
// If the class contains leading comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// # comment
|
||||
//
|
||||
// def func():
|
||||
// ...
|
||||
// ```
|
||||
//
|
||||
// At the top-level in a non-stub file, reformat as:
|
||||
// ```python
|
||||
// # comment
|
||||
//
|
||||
//
|
||||
// def func():
|
||||
// ...
|
||||
// ```
|
||||
// Note that this is only really relevant for the specific case in which there's a single
|
||||
// newline between the comment and the node, but we _require_ two newlines. If there are
|
||||
// _no_ newlines between the comment and the node, we don't insert _any_ newlines; if there
|
||||
// are more than two, then `leading_comments` will preserve the correct number of newlines.
|
||||
empty_lines_after_leading_comments(f, comments.leading(item)).fmt(f)?;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
|
||||
@@ -536,7 +536,7 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
|
||||
}
|
||||
|
||||
/// A statement representing a docstring.
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct DocstringStmt<'a>(&'a Stmt);
|
||||
|
||||
impl<'a> DocstringStmt<'a> {
|
||||
@@ -589,7 +589,7 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
|
||||
}
|
||||
|
||||
/// A Child of a suite.
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum SuiteChildStatement<'a> {
|
||||
/// A docstring documenting a class or function definition.
|
||||
Docstring(DocstringStmt<'a>),
|
||||
|
||||
@@ -327,7 +327,7 @@ fn write_suppressed_statements<'a>(
|
||||
|
||||
for range in CommentRangeIter::in_suppression(comments.trailing(statement), source) {
|
||||
match range {
|
||||
// All leading comments are suppressed
|
||||
// All trailing comments are suppressed
|
||||
// ```python
|
||||
// statement
|
||||
// # suppressed
|
||||
@@ -394,10 +394,14 @@ fn write_suppressed_statements<'a>(
|
||||
statement = SuiteChildStatement::Other(next_statement);
|
||||
leading_node_comments = comments.leading(next_statement);
|
||||
} else {
|
||||
let end = comments
|
||||
.trailing(statement)
|
||||
.last()
|
||||
.map_or(statement.end(), Ranged::end);
|
||||
let mut nodes =
|
||||
std::iter::successors(Some(AnyNodeRef::from(statement.statement())), |statement| {
|
||||
statement.last_child_in_body()
|
||||
});
|
||||
|
||||
let end = nodes
|
||||
.find_map(|statement| comments.trailing(statement).last().map(Ranged::end))
|
||||
.unwrap_or(statement.end());
|
||||
|
||||
FormatVerbatimStatementRange {
|
||||
verbatim_range: TextRange::new(format_off_comment.end(), end),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_formatter::FormatOptions;
|
||||
use ruff_python_formatter::{format_module_source, PyFormatOptions};
|
||||
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
|
||||
use similar::TextDiff;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::io::BufReader;
|
||||
@@ -142,16 +142,40 @@ fn format() {
|
||||
} else {
|
||||
let printed =
|
||||
format_module_source(&content, options.clone()).expect("Formatting to succeed");
|
||||
let formatted_code = printed.as_code();
|
||||
let formatted = printed.as_code();
|
||||
|
||||
ensure_stability_when_formatting_twice(formatted_code, options, input_path);
|
||||
ensure_stability_when_formatting_twice(formatted, options.clone(), input_path);
|
||||
|
||||
writeln!(
|
||||
snapshot,
|
||||
"## Output\n{}",
|
||||
CodeFrame::new("py", &formatted_code)
|
||||
)
|
||||
.unwrap();
|
||||
// We want to capture the differences in the preview style in our fixtures
|
||||
let options_preview = options.with_preview(PreviewMode::Enabled);
|
||||
let printed_preview = format_module_source(&content, options_preview.clone())
|
||||
.expect("Formatting to succeed");
|
||||
let formatted_preview = printed_preview.as_code();
|
||||
|
||||
ensure_stability_when_formatting_twice(
|
||||
formatted_preview,
|
||||
options_preview.clone(),
|
||||
input_path,
|
||||
);
|
||||
|
||||
if formatted == formatted_preview {
|
||||
writeln!(snapshot, "## Output\n{}", CodeFrame::new("py", &formatted)).unwrap();
|
||||
} else {
|
||||
// Having both snapshots makes it hard to see the difference, so we're keeping only
|
||||
// diff.
|
||||
writeln!(
|
||||
snapshot,
|
||||
"## Output\n{}\n## Preview changes\n{}",
|
||||
CodeFrame::new("py", &formatted),
|
||||
CodeFrame::new(
|
||||
"diff",
|
||||
TextDiff::from_lines(formatted, formatted_preview)
|
||||
.unified_diff()
|
||||
.header("Stable", "Preview")
|
||||
)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
insta::with_settings!({
|
||||
|
||||
@@ -162,7 +162,7 @@ def f():
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,29 +1,205 @@
|
||||
@@ -1,29 +1,206 @@
|
||||
+# This file doesn't use the standard decomposition.
|
||||
+# Decorator syntax test cases are separated by double # comments.
|
||||
+# Those before the 'output' comment are valid under the old syntax.
|
||||
@@ -172,6 +172,7 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -209,43 +210,48 @@ def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator()()
|
||||
+
|
||||
+@decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@(decorator)
|
||||
+
|
||||
+@decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@sequence["decorator"]
|
||||
+
|
||||
+@decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+
|
||||
+@dotted.decorator
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@var := decorator
|
||||
+
|
||||
+@dotted.decorator(arg)
|
||||
+def f():
|
||||
@@ -260,48 +266,43 @@ def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator()()
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@dotted.decorator(*args)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
##
|
||||
|
||||
-@(decorator)
|
||||
+
|
||||
+@dotted.decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
##
|
||||
|
||||
-@sequence["decorator"]
|
||||
+
|
||||
+@dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+
|
||||
+@dotted.decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
def f():
|
||||
...
|
||||
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
##
|
||||
|
||||
-@var := decorator
|
||||
+
|
||||
+@double.dotted.decorator
|
||||
+def f():
|
||||
@@ -387,6 +388,7 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator
|
||||
def f():
|
||||
...
|
||||
|
||||
@@ -5,6 +5,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellan
|
||||
## Input
|
||||
|
||||
```py
|
||||
# flags: --pyi
|
||||
from typing import Union
|
||||
|
||||
@bird
|
||||
@@ -42,7 +43,8 @@ def eggs() -> Union[str, int]: ...
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,32 +1,58 @@
|
||||
@@ -1,32 +1,59 @@
|
||||
+# flags: --pyi
|
||||
from typing import Union
|
||||
|
||||
+
|
||||
@@ -67,13 +69,13 @@ def eggs() -> Union[str, int]: ...
|
||||
- def BMethod(self, arg: List[str]) -> None: ...
|
||||
+ def BMethod(self, arg: List[str]) -> None:
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+class C:
|
||||
+ ...
|
||||
|
||||
-class C: ...
|
||||
|
||||
+class C:
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
@hmm
|
||||
-class D: ...
|
||||
+class D:
|
||||
@@ -118,6 +120,7 @@ def eggs() -> Union[str, int]: ...
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
# flags: --pyi
|
||||
from typing import Union
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/black/miscellan
|
||||
|
||||
```py
|
||||
''''''
|
||||
|
||||
'\''
|
||||
'"'
|
||||
"'"
|
||||
@@ -69,7 +70,7 @@ f"\"{a}\"{'hello' * b}\"{c}\""
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -24,7 +24,12 @@
|
||||
@@ -25,7 +25,12 @@
|
||||
r'Tricky "quote'
|
||||
r"Not-so-tricky \"quote"
|
||||
rf"{yay}"
|
||||
@@ -89,6 +90,7 @@ f"\"{a}\"{'hello' * b}\"{c}\""
|
||||
|
||||
```py
|
||||
""""""
|
||||
|
||||
"'"
|
||||
'"'
|
||||
"'"
|
||||
@@ -151,6 +153,7 @@ f"\"{a}\"{'hello' * b}\"{c}\""
|
||||
|
||||
```py
|
||||
""""""
|
||||
|
||||
"'"
|
||||
'"'
|
||||
"'"
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/py_39/python39.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
#!/usr/bin/env python3.9
|
||||
|
||||
@relaxed_decorator[0]
|
||||
def f():
|
||||
...
|
||||
|
||||
@relaxed_decorator[extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length]
|
||||
def f():
|
||||
...
|
||||
|
||||
@extremely_long_variable_name_that_doesnt_fit := complex.expression(with_long="arguments_value_that_wont_fit_at_the_end_of_the_line")
|
||||
def f():
|
||||
...
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3.9
|
||||
|
||||
-
|
||||
@relaxed_decorator[0]
|
||||
def f():
|
||||
...
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
#!/usr/bin/env python3.9
|
||||
|
||||
@relaxed_decorator[0]
|
||||
def f():
|
||||
...
|
||||
|
||||
|
||||
@relaxed_decorator[
|
||||
extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length
|
||||
]
|
||||
def f():
|
||||
...
|
||||
|
||||
|
||||
@extremely_long_variable_name_that_doesnt_fit := complex.expression(
|
||||
with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
|
||||
)
|
||||
def f():
|
||||
...
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
#!/usr/bin/env python3.9
|
||||
|
||||
|
||||
@relaxed_decorator[0]
|
||||
def f():
|
||||
...
|
||||
|
||||
|
||||
@relaxed_decorator[
|
||||
extremely_long_name_that_definitely_will_not_fit_on_one_line_of_standard_length
|
||||
]
|
||||
def f():
|
||||
...
|
||||
|
||||
|
||||
@extremely_long_variable_name_that_doesnt_fit := complex.expression(
|
||||
with_long="arguments_value_that_wont_fit_at_the_end_of_the_line"
|
||||
)
|
||||
def f():
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/raw_docstring.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
# flags: --preview --skip-string-normalization
|
||||
class C:
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
def f():
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
class SingleQuotes:
|
||||
|
||||
|
||||
r'''Raw'''
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,4 +1,6 @@
|
||||
+# flags: --preview --skip-string-normalization
|
||||
class C:
|
||||
+
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
@@ -7,8 +9,9 @@
|
||||
|
||||
|
||||
class SingleQuotes:
|
||||
- r'''Raw'''
|
||||
|
||||
+ r"""Raw"""
|
||||
+
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
# flags: --preview --skip-string-normalization
|
||||
class C:
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
def f():
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
class SingleQuotes:
|
||||
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
class C:
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
def f():
|
||||
r"""Raw"""
|
||||
|
||||
|
||||
class SingleQuotes:
|
||||
r'''Raw'''
|
||||
|
||||
|
||||
class UpperCaseR:
|
||||
R"""Raw"""
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary_pow_spacing.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# No spacing
|
||||
5 ** 5
|
||||
5.0 ** 5.0
|
||||
1e5 ** 2e5
|
||||
True ** True
|
||||
False ** False
|
||||
None ** None
|
||||
|
||||
# Space
|
||||
"a" ** "b"
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
# No spacing
|
||||
5**5
|
||||
5.0**5.0
|
||||
1e5**2e5
|
||||
True**True
|
||||
False**False
|
||||
None**None
|
||||
|
||||
# Space
|
||||
"a" ** "b"
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -186,6 +186,19 @@ if "root" not in (
|
||||
):
|
||||
msg = "Could not find root. Please try a different forest."
|
||||
raise ValueError(msg)
|
||||
|
||||
# Regression for https://github.com/astral-sh/ruff/issues/8183
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -292,10 +305,13 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & (
|
||||
):
|
||||
pass
|
||||
|
||||
if not (
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
) & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
|
||||
if (
|
||||
not (
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
)
|
||||
& aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
@@ -383,6 +399,21 @@ if "root" not in (
|
||||
):
|
||||
msg = "Could not find root. Please try a different forest."
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
# Regression for https://github.com/astral-sh/ruff/issues/8183
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa(bbbbbbbb, ccccccc)) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def foo():
|
||||
while (
|
||||
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_unclosed_deep_nested_trailing_comment.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
if True:
|
||||
if False:
|
||||
x: int # Optional[int]
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
if True:
|
||||
if False:
|
||||
x: int # Optional[int]
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off/fmt_off_unclosed_trailing_comment.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
x: int # Optional[int]
|
||||
```
|
||||
|
||||
## Output
|
||||
```py
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/8211
|
||||
|
||||
# fmt: off
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
x: int # Optional[int]
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ def test():
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -93,4 +93,21 @@ def test3 ():
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
|
||||
# formatted
|
||||
-def test2():
|
||||
- ...
|
||||
+def test2(): ...
|
||||
|
||||
|
||||
a = 10
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -549,4 +549,27 @@ if True:
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -245,13 +245,11 @@
|
||||
class Path:
|
||||
if sys.version_info >= (3, 11):
|
||||
|
||||
- def joinpath(self):
|
||||
- ...
|
||||
+ def joinpath(self): ...
|
||||
|
||||
# The .open method comes from pathlib.pyi and should be kept in sync.
|
||||
@overload
|
||||
- def open(self):
|
||||
- ...
|
||||
+ def open(self): ...
|
||||
|
||||
|
||||
def fakehttp():
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# Below is black stable style
|
||||
# In preview style, black always breaks the right side first
|
||||
"""
|
||||
Black's `Preview.module_docstring_newlines`
|
||||
"""
|
||||
first_stmt_after_module_level_docstring = 1
|
||||
|
||||
if True:
|
||||
|
||||
class CachedRepository:
|
||||
# Black's `Preview.dummy_implementations`
|
||||
def get_release_info(self): ...
|
||||
|
||||
|
||||
def raw_docstring():
|
||||
|
||||
r"""Black's `Preview.accept_raw_docstrings`
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def reference_docstring_newlines():
|
||||
|
||||
"""A regular docstring for comparison
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RemoveNewlineBeforeClassDocstring:
|
||||
|
||||
"""Black's `Preview.no_blank_line_before_class_docstring`"""
|
||||
|
||||
|
||||
def f():
|
||||
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
] = cccccccc.ccccccccccccc.cccccccc
|
||||
@@ -52,10 +84,41 @@ preview = Disabled
|
||||
```
|
||||
|
||||
```py
|
||||
# Below is black stable style
|
||||
# In preview style, black always breaks the right side first
|
||||
"""
|
||||
Black's `Preview.module_docstring_newlines`
|
||||
"""
|
||||
first_stmt_after_module_level_docstring = 1
|
||||
|
||||
if True:
|
||||
|
||||
class CachedRepository:
|
||||
# Black's `Preview.dummy_implementations`
|
||||
def get_release_info(self):
|
||||
...
|
||||
|
||||
|
||||
def raw_docstring():
|
||||
r"""Black's `Preview.accept_raw_docstrings`
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def reference_docstring_newlines():
|
||||
"""A regular docstring for comparison
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RemoveNewlineBeforeClassDocstring:
|
||||
|
||||
"""Black's `Preview.no_blank_line_before_class_docstring`"""
|
||||
|
||||
|
||||
def f():
|
||||
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
] = cccccccc.ccccccccccccc.cccccccc
|
||||
@@ -100,10 +163,40 @@ preview = Enabled
|
||||
```
|
||||
|
||||
```py
|
||||
# Below is black stable style
|
||||
# In preview style, black always breaks the right side first
|
||||
"""
|
||||
Black's `Preview.module_docstring_newlines`
|
||||
"""
|
||||
first_stmt_after_module_level_docstring = 1
|
||||
|
||||
if True:
|
||||
|
||||
class CachedRepository:
|
||||
# Black's `Preview.dummy_implementations`
|
||||
def get_release_info(self): ...
|
||||
|
||||
|
||||
def raw_docstring():
|
||||
r"""Black's `Preview.accept_raw_docstrings`
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def reference_docstring_newlines():
|
||||
"""A regular docstring for comparison
|
||||
a
|
||||
b
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RemoveNewlineBeforeClassDocstring:
|
||||
|
||||
"""Black's `Preview.no_blank_line_before_class_docstring`"""
|
||||
|
||||
|
||||
def f():
|
||||
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
] = cccccccc.ccccccccccccc.cccccccc
|
||||
@@ -7,6 +7,21 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/
|
||||
# Regression test: Don't forget the parentheses in the value when breaking
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int = a + 1 * a
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
)
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
)= Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
|
||||
JSONSerializable: TypeAlias = (
|
||||
"str | int | float | bool | None | list | tuple | JSONMapping"
|
||||
)
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {1, 2, 3, 4}
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = aaaaaaaaaaaaaaaa
|
||||
|
||||
# Regression test: Don't forget the parentheses in the annotation when breaking
|
||||
class DefaultRunner:
|
||||
@@ -20,12 +35,35 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: int =
|
||||
a + 1 * a
|
||||
)
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
)
|
||||
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: (
|
||||
Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
) = Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb()
|
||||
|
||||
JSONSerializable: TypeAlias = (
|
||||
"str | int | float | bool | None | list | tuple | JSONMapping"
|
||||
)
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = {
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
}
|
||||
|
||||
JSONSerializable: str | int | float | bool | None | list | tuple | JSONMapping = (
|
||||
aaaaaaaaaaaaaaaa
|
||||
)
|
||||
|
||||
|
||||
# Regression test: Don't forget the parentheses in the annotation when breaking
|
||||
class DefaultRunner:
|
||||
task_runner_cls: (
|
||||
TaskRunnerProtocol | typing.Callable[[], typing.Any]
|
||||
) = DefaultTaskRunner
|
||||
task_runner_cls: TaskRunnerProtocol | typing.Callable[
|
||||
[], typing.Any
|
||||
] = DefaultTaskRunner
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
# comment
|
||||
|
||||
class Test(
|
||||
Aaaaaaaaaaaaaaaaa,
|
||||
Bbbbbbbbbbbbbbbb,
|
||||
@@ -232,6 +234,9 @@ class QuerySet(AltersData):
|
||||
|
||||
## Output
|
||||
```py
|
||||
# comment
|
||||
|
||||
|
||||
class Test(
|
||||
Aaaaaaaaaaaaaaaaa,
|
||||
Bbbbbbbbbbbbbbbb,
|
||||
@@ -494,4 +499,45 @@ class QuerySet(AltersData):
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -28,8 +28,7 @@
|
||||
pass
|
||||
|
||||
|
||||
-class Test((Aaaa)):
|
||||
- ...
|
||||
+class Test((Aaaa)): ...
|
||||
|
||||
|
||||
class Test(
|
||||
@@ -159,20 +158,17 @@
|
||||
|
||||
@dataclass
|
||||
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
|
||||
-class AltCLIPOutput(ModelOutput):
|
||||
- ...
|
||||
+class AltCLIPOutput(ModelOutput): ...
|
||||
|
||||
|
||||
@dataclass
|
||||
-class AltCLIPOutput: # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
|
||||
- ...
|
||||
+class AltCLIPOutput: ... # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
|
||||
|
||||
|
||||
@dataclass
|
||||
class AltCLIPOutput(
|
||||
# Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->AltCLIP
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
class TestTypeParams[
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -996,4 +996,167 @@ def default_arg_comments2( #
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -2,8 +2,7 @@
|
||||
def test(
|
||||
# comment
|
||||
# another
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
# Argument empty line spacing
|
||||
@@ -12,8 +11,7 @@
|
||||
a,
|
||||
# another
|
||||
b,
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
### Different function argument wrappings
|
||||
@@ -57,8 +55,7 @@
|
||||
b,
|
||||
# comment
|
||||
*args,
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def kwarg_with_leading_comments(
|
||||
@@ -66,8 +63,7 @@
|
||||
b,
|
||||
# comment
|
||||
**kwargs,
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def argument_with_long_default(
|
||||
@@ -75,8 +71,7 @@
|
||||
b=ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
|
||||
+ [dddddddddddddddddddd, eeeeeeeeeeeeeeeeeeee, ffffffffffffffffffffffff],
|
||||
h=[],
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def argument_with_long_type_annotation(
|
||||
@@ -85,12 +80,10 @@
|
||||
| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
| zzzzzzzzzzzzzzzzzzz = [0, 1, 2, 3],
|
||||
h=[],
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
-def test():
|
||||
- ...
|
||||
+def test(): ...
|
||||
|
||||
|
||||
# Type parameter empty line spacing
|
||||
@@ -99,8 +92,7 @@
|
||||
A,
|
||||
# another
|
||||
B,
|
||||
-]():
|
||||
- ...
|
||||
+](): ...
|
||||
|
||||
|
||||
# Type parameter comments
|
||||
@@ -159,8 +151,7 @@
|
||||
|
||||
|
||||
# Comment
|
||||
-def with_leading_comment():
|
||||
- ...
|
||||
+def with_leading_comment(): ...
|
||||
|
||||
|
||||
# Comment that could be mistaken for a trailing comment of the function declaration when
|
||||
@@ -192,8 +183,7 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/5176#issuecomment-1598171989
|
||||
def foo(
|
||||
b=3 + 2, # comment
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
# Comments on the slash or the star, both of which don't have a node
|
||||
@@ -454,8 +444,7 @@
|
||||
def f(
|
||||
# first
|
||||
# second
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def f( # first
|
||||
@@ -475,8 +464,7 @@
|
||||
# first
|
||||
b,
|
||||
# second
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def f( # first
|
||||
@@ -484,8 +472,7 @@
|
||||
# second
|
||||
b,
|
||||
# third
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def f( # first
|
||||
@@ -494,8 +481,7 @@
|
||||
# third
|
||||
b,
|
||||
# fourth
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def f( # first
|
||||
@@ -522,17 +508,14 @@
|
||||
a,
|
||||
# third
|
||||
/, # second
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
# Walrus operator in return type.
|
||||
-def this_is_unusual() -> (please := no):
|
||||
- ...
|
||||
+def this_is_unusual() -> (please := no): ...
|
||||
|
||||
|
||||
-def this_is_unusual(x) -> (please := no):
|
||||
- ...
|
||||
+def this_is_unusual(x) -> (please := no): ...
|
||||
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7465
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,38 @@ return (
|
||||
len(node.parents) for node in self.node_map.values()
|
||||
)
|
||||
)
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8042
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv" +
|
||||
"text/csv" +
|
||||
output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv" + "text/csv",
|
||||
output.getvalue().encode("utf-8----------------")
|
||||
)
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv",
|
||||
"text/csv",
|
||||
output.getvalue().encode("utf-8----------------")
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv" + "text/csv" + output.getvalue().encode("utf-8----------------"),
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv" + "text/csv", output.getvalue().encode("utf-8----------------")
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv", "text/csv", output.getvalue().encode("utf-8----------------")
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -38,6 +70,54 @@ return (
|
||||
len(self.nodeseeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee),
|
||||
sum(len(node.parents) for node in self.node_map.values()),
|
||||
)
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/8042
|
||||
def f():
|
||||
return (
|
||||
self.get_filename()
|
||||
+ ".csv"
|
||||
+ "text/csv"
|
||||
+ output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv" + "text/csv",
|
||||
output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv",
|
||||
"text/csv",
|
||||
output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename()
|
||||
+ ".csv"
|
||||
+ "text/csv"
|
||||
+ output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return self.get_filename() + ".csv" + "text/csv", output.getvalue().encode(
|
||||
"utf-8----------------"
|
||||
)
|
||||
|
||||
|
||||
def f():
|
||||
return (
|
||||
self.get_filename() + ".csv",
|
||||
"text/csv",
|
||||
output.getvalue().encode("utf-8----------------"),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -544,4 +544,298 @@ def process_board_action(
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -7,8 +7,7 @@
|
||||
start: int | None = None,
|
||||
num: int | None = None,
|
||||
) -> ( # type: ignore[override]
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def zrevrangebylex(
|
||||
@@ -20,8 +19,7 @@
|
||||
num: int | None = None,
|
||||
) -> ( # type: ignore[override]
|
||||
# comment
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def zrevrangebylex(
|
||||
@@ -33,8 +31,7 @@
|
||||
num: int | None = None,
|
||||
) -> ( # type: ignore[override]
|
||||
1
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def zrevrangebylex(
|
||||
@@ -47,8 +44,7 @@
|
||||
) -> ( # type: ignore[override]
|
||||
1,
|
||||
2,
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def zrevrangebylex(
|
||||
@@ -60,14 +56,12 @@
|
||||
num: int | None = None,
|
||||
) -> ( # type: ignore[override]
|
||||
(1, 2)
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
-) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
- ...
|
||||
+) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]: ...
|
||||
|
||||
|
||||
def double(
|
||||
@@ -95,50 +89,44 @@
|
||||
# function arguments break here with a single argument; we do not.)
|
||||
def f(
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
|
||||
- ...
|
||||
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
|
||||
|
||||
|
||||
def f(
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a
|
||||
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
|
||||
- ...
|
||||
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
|
||||
|
||||
|
||||
def f(
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-) -> a:
|
||||
- ...
|
||||
+) -> a: ...
|
||||
|
||||
|
||||
def f(
|
||||
a
|
||||
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
|
||||
- ...
|
||||
+) -> (
|
||||
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
+): ...
|
||||
|
||||
|
||||
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> (
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def f[
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-]() -> a:
|
||||
- ...
|
||||
+]() -> a: ...
|
||||
|
||||
|
||||
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
|
||||
- ...
|
||||
+) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ...
|
||||
|
||||
|
||||
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-) -> a:
|
||||
- ...
|
||||
+) -> a: ...
|
||||
|
||||
|
||||
# Breaking return type annotations. Black adds parentheses if the parameters are
|
||||
@@ -147,137 +135,126 @@
|
||||
Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
]
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
]
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
]
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
]
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
) -> Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
-]:
|
||||
- ...
|
||||
+]: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
) -> Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
-]:
|
||||
- ...
|
||||
+]: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
*args
|
||||
) -> Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
-]:
|
||||
- ...
|
||||
+]: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx( # foo
|
||||
) -> Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
-]:
|
||||
- ...
|
||||
+]: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
# bar
|
||||
) -> Set[
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
-]:
|
||||
- ...
|
||||
+]: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
|
||||
- ...
|
||||
+) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
-) -> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:
|
||||
- ...
|
||||
+) -> (
|
||||
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
+): ...
|
||||
|
||||
|
||||
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X + Y + foooooooooooooooooooooooooooooooooooo():
|
||||
- ...
|
||||
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
+ X + Y + foooooooooooooooooooooooooooooooooooo()
|
||||
+): ...
|
||||
|
||||
|
||||
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X + Y + foooooooooooooooooooooooooooooooooooo():
|
||||
- ...
|
||||
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
+ x
|
||||
+) -> X + Y + foooooooooooooooooooooooooooooooooooo(): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
X and Y and foooooooooooooooooooooooooooooooooooo()
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
-) -> X and Y and foooooooooooooooooooooooooooooooooooo():
|
||||
- ...
|
||||
+) -> X and Y and foooooooooooooooooooooooooooooooooooo(): ...
|
||||
|
||||
|
||||
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> X | Y | foooooooooooooooooooooooooooooooooooo():
|
||||
- ...
|
||||
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
+ X | Y | foooooooooooooooooooooooooooooooooooo()
|
||||
+): ...
|
||||
|
||||
|
||||
-def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> X | Y | foooooooooooooooooooooooooooooooooooo():
|
||||
- ...
|
||||
+def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
+ x
|
||||
+) -> X | Y | foooooooooooooooooooooooooooooooooooo(): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
|
||||
X | Y | foooooooooooooooooooooooooooooooooooo() # comment
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
|
||||
x
|
||||
) -> (
|
||||
X | Y | foooooooooooooooooooooooooooooooooooo() # comment
|
||||
-):
|
||||
- ...
|
||||
+): ...
|
||||
|
||||
|
||||
def double() -> (
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -115,4 +115,57 @@ def quuz():
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -12,25 +12,20 @@
|
||||
pass
|
||||
|
||||
|
||||
-class Del(expr_context):
|
||||
- ...
|
||||
+class Del(expr_context): ...
|
||||
|
||||
|
||||
-class Load(expr_context):
|
||||
- ...
|
||||
+class Load(expr_context): ...
|
||||
|
||||
|
||||
# Some comment.
|
||||
-class Other(expr_context):
|
||||
- ...
|
||||
+class Other(expr_context): ...
|
||||
|
||||
|
||||
-class Store(expr_context):
|
||||
- ...
|
||||
+class Store(expr_context): ...
|
||||
|
||||
|
||||
-class Foo(Bar):
|
||||
- ...
|
||||
+class Foo(Bar): ...
|
||||
|
||||
|
||||
class Baz(Qux):
|
||||
@@ -49,12 +44,10 @@
|
||||
pass
|
||||
|
||||
|
||||
-def bar():
|
||||
- ...
|
||||
+def bar(): ...
|
||||
|
||||
|
||||
-def baz():
|
||||
- ...
|
||||
+def baz(): ...
|
||||
|
||||
|
||||
def quux():
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_parser::lexer::{lex, LexicalError};
|
||||
use ruff_python_parser::{Mode, Tok};
|
||||
use ruff_python_parser::{AsMode, Tok};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
@@ -25,11 +26,12 @@ impl CommentRangesBuilder {
|
||||
/// Helper method to lex and extract comment ranges
|
||||
pub fn tokens_and_ranges(
|
||||
source: &str,
|
||||
source_type: PySourceType,
|
||||
) -> Result<(Vec<(Tok, TextRange)>, CommentRanges), LexicalError> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut comment_ranges = CommentRangesBuilder::default();
|
||||
|
||||
for result in lex(source, Mode::Module) {
|
||||
for result in lex(source, source_type.as_mode()) {
|
||||
let (token, range) = result?;
|
||||
|
||||
comment_ranges.visit_token(&token, range);
|
||||
|
||||
@@ -5,8 +5,11 @@ use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
/// Stores the ranges of all f-strings in a file sorted by [`TextRange::start`].
|
||||
/// There can be multiple overlapping ranges for nested f-strings.
|
||||
///
|
||||
/// Note that the ranges for all unterminated f-strings are not stored.
|
||||
#[derive(Debug)]
|
||||
pub struct FStringRanges {
|
||||
// Mapping from the f-string start location to its range.
|
||||
raw: BTreeMap<TextSize, TextRange>,
|
||||
}
|
||||
|
||||
@@ -89,7 +92,6 @@ impl FStringRangesBuilder {
|
||||
}
|
||||
|
||||
pub(crate) fn finish(self) -> FStringRanges {
|
||||
debug_assert!(self.start_locations.is_empty());
|
||||
FStringRanges { raw: self.raw }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ bitflags = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
lalrpop-util = { version = "0.20.0", default-features = false }
|
||||
memchr = { workspace = true }
|
||||
unicode-ident = { workspace = true }
|
||||
unicode_names2 = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
@@ -407,7 +407,9 @@ impl<'source> Lexer<'source> {
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert_eq!(self.cursor.previous(), '#');
|
||||
|
||||
self.cursor.eat_while(|c| !matches!(c, '\n' | '\r'));
|
||||
let bytes = self.cursor.rest().as_bytes();
|
||||
let offset = memchr::memchr2(b'\n', b'\r', bytes).unwrap_or(bytes.len());
|
||||
self.cursor.skip_bytes(offset);
|
||||
|
||||
Tok::Comment(self.token_text().to_string())
|
||||
}
|
||||
|
||||
@@ -127,4 +127,21 @@ impl<'a> Cursor<'a> {
|
||||
self.bump();
|
||||
}
|
||||
}
|
||||
|
||||
/// Skips the next `count` bytes.
|
||||
///
|
||||
/// ## Panics
|
||||
/// - If `count` is larger than the remaining bytes in the input stream.
|
||||
/// - If `count` indexes into a multi-byte character.
|
||||
pub(super) fn skip_bytes(&mut self, count: usize) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.prev_char = self.chars.as_str()[..count]
|
||||
.chars()
|
||||
.next_back()
|
||||
.unwrap_or('\0');
|
||||
}
|
||||
|
||||
self.chars = self.chars.as_str()[count..].chars();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -24,8 +24,8 @@ use ruff_linter::rule_selector::{PreviewOptions, Specificity};
|
||||
use ruff_linter::rules::pycodestyle;
|
||||
use ruff_linter::settings::rule_table::RuleTable;
|
||||
use ruff_linter::settings::types::{
|
||||
CodePattern, FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion,
|
||||
SerializationFormat, UnsafeFixes, Version,
|
||||
FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
|
||||
UnsafeFixes, Version,
|
||||
};
|
||||
use ruff_linter::settings::{
|
||||
resolve_per_file_ignores, LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS,
|
||||
@@ -226,17 +226,9 @@ impl Configuration {
|
||||
dummy_variable_rgx: lint
|
||||
.dummy_variable_rgx
|
||||
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
|
||||
external: lint
|
||||
.external
|
||||
.map(|code| {
|
||||
code.into_iter()
|
||||
.map(|name| CodePattern::new(&name))
|
||||
.collect()
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Invalid `external` value: {e}"))?
|
||||
.unwrap_or_default(),
|
||||
external: lint.external.unwrap_or_default(),
|
||||
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(),
|
||||
line_length,
|
||||
tab_size: self.indent_width.unwrap_or_default(),
|
||||
namespace_packages: self.namespace_packages.unwrap_or_default(),
|
||||
per_file_ignores: resolve_per_file_ignores(
|
||||
|
||||
@@ -352,7 +352,7 @@ pub struct Options {
|
||||
|
||||
// Global Formatting options
|
||||
/// The line length to use when enforcing long-lines violations (like `E501`)
|
||||
/// and at which the formatter prefers to wrap lines.
|
||||
/// and at which `isort` and the formatter prefers to wrap lines.
|
||||
///
|
||||
/// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
|
||||
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
|
||||
@@ -548,20 +548,17 @@ pub struct LintCommonOptions {
|
||||
)]
|
||||
pub extend_unfixable: Option<Vec<RuleSelector>>,
|
||||
|
||||
/// A list of rule codes that are unsupported by Ruff, but should be
|
||||
/// A list of rule codes or prefixes that are unsupported by Ruff, but should be
|
||||
/// preserved when (e.g.) validating `# noqa` directives. Useful for
|
||||
/// retaining `# noqa` directives that cover plugins not yet implemented
|
||||
/// by Ruff.
|
||||
///
|
||||
/// Supports glob patterns. For more information on the glob syntax, refer
|
||||
/// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
|
||||
#[option(
|
||||
default = "[]",
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
# Avoiding flagging (and removing) `V101` from any `# noqa`
|
||||
# directives, despite Ruff's lack of support for `vulture`.
|
||||
external = ["V101"]
|
||||
# Avoiding flagging (and removing) any codes starting with `V` from any
|
||||
# `# noqa` directives, despite Ruff's lack of support for `vulture`.
|
||||
external = ["V"]
|
||||
"#
|
||||
)]
|
||||
pub external: Option<Vec<String>>,
|
||||
@@ -1415,6 +1412,9 @@ impl Flake8PytestStyleOptions {
|
||||
pub struct Flake8QuotesOptions {
|
||||
/// Quote style to prefer for inline strings (either "single" or
|
||||
/// "double").
|
||||
///
|
||||
/// When using the formatter, ensure that `format.quote-style` is set to
|
||||
/// the same preferred quote style.
|
||||
#[option(
|
||||
default = r#""double""#,
|
||||
value_type = r#""single" | "double""#,
|
||||
@@ -1426,6 +1426,9 @@ pub struct Flake8QuotesOptions {
|
||||
|
||||
/// Quote style to prefer for multiline strings (either "single" or
|
||||
/// "double").
|
||||
///
|
||||
/// When using the formatter, only "double" is compatible, as the formatter
|
||||
/// enforces double quotes for multiline strings.
|
||||
#[option(
|
||||
default = r#""double""#,
|
||||
value_type = r#""single" | "double""#,
|
||||
@@ -1436,6 +1439,9 @@ pub struct Flake8QuotesOptions {
|
||||
pub multiline_quotes: Option<Quote>,
|
||||
|
||||
/// Quote style to prefer for docstrings (either "single" or "double").
|
||||
///
|
||||
/// When using the formatter, only "double" is compatible, as the formatter
|
||||
/// enforces double quotes for docstrings strings.
|
||||
#[option(
|
||||
default = r#""double""#,
|
||||
value_type = r#""single" | "double""#,
|
||||
@@ -1686,6 +1692,9 @@ pub struct IsortOptions {
|
||||
/// `combine-as-imports = true`. When `combine-as-imports` isn't
|
||||
/// enabled, every aliased `import from` will be given its own line, in
|
||||
/// which case, wrapping is not necessary.
|
||||
///
|
||||
/// When using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default)
|
||||
/// when enabling `force-wrap-aliases` to avoid that the formatter collapses members if they all fit on a single line.
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
value_type = "bool",
|
||||
@@ -1729,6 +1738,9 @@ pub struct IsortOptions {
|
||||
/// the imports will never be folded into one line.
|
||||
///
|
||||
/// See isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.
|
||||
///
|
||||
/// When using the formatter, ensure that `format.skip-magic-trailing-comma` is set to `false` (default) when enabling `split-on-trailing-comma`
|
||||
/// to avoid that the formatter removes the trailing commas.
|
||||
#[option(
|
||||
default = r#"true"#,
|
||||
value_type = "bool",
|
||||
@@ -1910,6 +1922,9 @@ pub struct IsortOptions {
|
||||
|
||||
/// The number of blank lines to place after imports.
|
||||
/// Use `-1` for automatic determination.
|
||||
///
|
||||
/// When using the formatter, only the values `-1`, `1`, and `2` are compatible because
|
||||
/// it enforces at least one empty and at most two empty lines after imports.
|
||||
#[option(
|
||||
default = r#"-1"#,
|
||||
value_type = "int",
|
||||
@@ -1921,6 +1936,9 @@ pub struct IsortOptions {
|
||||
pub lines_after_imports: Option<isize>,
|
||||
|
||||
/// The number of lines to place between "direct" and `import from` imports.
|
||||
///
|
||||
/// When using the formatter, only the values `0` and `1` are compatible because
|
||||
/// it preserves up to one empty line after imports in nested blocks.
|
||||
#[option(
|
||||
default = r#"0"#,
|
||||
value_type = "int",
|
||||
@@ -2304,7 +2322,7 @@ pub struct PycodestyleOptions {
|
||||
/// documentation (`W505`), including standalone comments. By default,
|
||||
/// this is set to null which disables reporting violations.
|
||||
///
|
||||
/// The length is determined by the number of characters per line, except for lines containinAsian characters or emojis.
|
||||
/// The length is determined by the number of characters per line, except for lines containing Asian characters or emojis.
|
||||
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
|
||||
///
|
||||
/// See the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, bail};
|
||||
use globset::{Candidate, GlobSet};
|
||||
use ignore::{WalkBuilder, WalkState};
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
@@ -333,12 +334,18 @@ pub fn python_files_in_path(
|
||||
let resolver = resolver.read().unwrap();
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
|
||||
let file_path = Candidate::new(path);
|
||||
let file_basename = Candidate::new(file_name);
|
||||
if match_candidate_exclusion(
|
||||
&file_path,
|
||||
&file_basename,
|
||||
&settings.file_resolver.exclude,
|
||||
) {
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
return WalkState::Skip;
|
||||
} else if match_exclusion(
|
||||
path,
|
||||
file_name,
|
||||
} else if match_candidate_exclusion(
|
||||
&file_path,
|
||||
&file_basename,
|
||||
&settings.file_resolver.extend_exclude,
|
||||
) {
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
@@ -509,10 +516,20 @@ fn is_file_excluded(
|
||||
for path in path.ancestors() {
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if let Some(file_name) = path.file_name() {
|
||||
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
|
||||
let file_path = Candidate::new(path);
|
||||
let file_basename = Candidate::new(file_name);
|
||||
if match_candidate_exclusion(
|
||||
&file_path,
|
||||
&file_basename,
|
||||
&settings.file_resolver.exclude,
|
||||
) {
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
return true;
|
||||
} else if match_exclusion(path, file_name, &settings.file_resolver.extend_exclude) {
|
||||
} else if match_candidate_exclusion(
|
||||
&file_path,
|
||||
&file_basename,
|
||||
&settings.file_resolver.extend_exclude,
|
||||
) {
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
return true;
|
||||
}
|
||||
@@ -533,11 +550,27 @@ fn is_file_excluded(
|
||||
pub fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
|
||||
file_path: P,
|
||||
file_basename: R,
|
||||
exclusion: &globset::GlobSet,
|
||||
exclusion: &GlobSet,
|
||||
) -> bool {
|
||||
if exclusion.is_empty() {
|
||||
return false;
|
||||
}
|
||||
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
|
||||
}
|
||||
|
||||
/// Return `true` if the given candidates should be ignored based on the exclusion
|
||||
/// criteria.
|
||||
pub fn match_candidate_exclusion(
|
||||
file_path: &Candidate,
|
||||
file_basename: &Candidate,
|
||||
exclusion: &GlobSet,
|
||||
) -> bool {
|
||||
if exclusion.is_empty() {
|
||||
return false;
|
||||
}
|
||||
exclusion.is_match_candidate(file_path) || exclusion.is_match_candidate(file_basename)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{create_dir, File};
|
||||
|
||||
@@ -67,7 +67,7 @@ quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
magic-trailing-comma = "respect"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
@@ -78,7 +78,7 @@ As an example, the following would configure Ruff to:
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
|
||||
select = ["E", "F", "B"]
|
||||
select = ["E4", "E7", "E9", "F", "B"]
|
||||
|
||||
# 2. Avoid enforcing line-length violations (`E501`)
|
||||
ignore = ["E501"]
|
||||
@@ -101,7 +101,7 @@ Linter plugin configurations are expressed as subsections, e.g.:
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
# Add "Q" to the list of enabled codes.
|
||||
select = ["E", "F", "Q"]
|
||||
select = ["E4", "E7", "E9", "F", "Q"]
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
@@ -121,7 +121,7 @@ For example, the `pyproject.toml` described above would be represented via the f
|
||||
```toml
|
||||
[lint]
|
||||
# Enable flake8-bugbear (`B`) rules.
|
||||
select = ["E", "F", "B"]
|
||||
select = ["E4", "E7", "E9", "F", "B"]
|
||||
|
||||
# Never enforce `E501` (line length violations).
|
||||
ignore = ["E501"]
|
||||
|
||||
@@ -164,10 +164,10 @@ elif False: # fmt: skip
|
||||
pass
|
||||
|
||||
@Test
|
||||
@Test2 # fmt: off
|
||||
@Test2 # fmt: skip
|
||||
def test(): ...
|
||||
|
||||
a = [1, 2, 3, 4, 5] # fmt: off
|
||||
a = [1, 2, 3, 4, 5] # fmt: skip
|
||||
|
||||
def test(a, b, c, d, e, f) -> int: # fmt: skip
|
||||
pass
|
||||
@@ -180,9 +180,10 @@ comments, which are treated equivalently to `# fmt: off` and `# fmt: on`, respec
|
||||
|
||||
Ruff's formatter is designed to be used alongside the linter. However, the linter includes
|
||||
some rules that, when enabled, can cause conflicts with the formatter, leading to unexpected
|
||||
behavior.
|
||||
behavior. When configured appropriately, the goal of Ruff's formatter-linter compatibility is
|
||||
such that running the formatter should never introduce new lint errors.
|
||||
|
||||
When using Ruff as a formatter, we recommend disabling the following rules:
|
||||
As such, when using Ruff as a formatter, we recommend avoiding the following lint rules:
|
||||
|
||||
- [`tab-indentation`](rules/tab-indentation.md) (`W191`)
|
||||
- [`indentation-with-invalid-multiple`](rules/indentation-with-invalid-multiple.md) (`E111`)
|
||||
@@ -199,7 +200,11 @@ When using Ruff as a formatter, we recommend disabling the following rules:
|
||||
- [`single-line-implicit-string-concatenation`](rules/single-line-implicit-string-concatenation.md) (`ISC001`)
|
||||
- [`multi-line-implicit-string-concatenation`](rules/multi-line-implicit-string-concatenation.md) (`ISC002`)
|
||||
|
||||
Similarly, we recommend disabling the following isort settings, which are incompatible with the
|
||||
None of the above are included in Ruff's default configuration. However, if you've enabled
|
||||
any of these rules or their parent categories (like `Q`), we recommend disabling them via the
|
||||
linter's [`ignore`](settings.md#ignore) setting.
|
||||
|
||||
Similarly, we recommend avoiding the following isort settings, which are incompatible with the
|
||||
formatter's treatment of import statements when set to non-default values:
|
||||
|
||||
- [`force-single-line`](settings.md#isort-force-single-line)
|
||||
@@ -208,6 +213,12 @@ formatter's treatment of import statements when set to non-default values:
|
||||
- [`lines-between-types`](settings.md#isort-lines-between-types)
|
||||
- [`split-on-trailing-comma`](settings.md#isort-split-on-trailing-comma)
|
||||
|
||||
If you've configured any of these settings to take on non-default values, we recommend removing
|
||||
them from your Ruff configuration.
|
||||
|
||||
When an incompatible lint rule or setting is enabled, `ruff format` will emit a warning. If your
|
||||
`ruff format` is free of warnings, you're good to go!
|
||||
|
||||
## Exit codes
|
||||
|
||||
`ruff format` exits with the following status codes:
|
||||
|
||||
@@ -399,28 +399,6 @@ def update_emission_strength():
|
||||
value = self.emission_strength * 2
|
||||
```
|
||||
|
||||
### Type annotations may be parenthesized when expanded
|
||||
|
||||
Black will avoid parenthesizing type annotations in an annotated assignment, while Ruff will insert
|
||||
parentheses in some cases.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
# Black
|
||||
StartElementHandler: Callable[[str, dict[str, str]], Any] | Callable[[str, list[str]], Any] | Callable[
|
||||
[str, dict[str, str], list[str]], Any
|
||||
] | None
|
||||
|
||||
# Ruff
|
||||
StartElementHandler: (
|
||||
Callable[[str, dict[str, str]], Any]
|
||||
| Callable[[str, list[str]], Any]
|
||||
| Callable[[str, dict[str, str], list[str]], Any]
|
||||
| None
|
||||
)
|
||||
```
|
||||
|
||||
### Call chain calls break differently
|
||||
|
||||
Black occasionally breaks call chains differently than Ruff; in particular, Black occasionally
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user