Compare commits

..

3 Commits

Author SHA1 Message Date
Charlie Marsh
292bf7fd15 Rebase 2023-11-03 00:43:44 -04:00
Charlie Marsh
25bccf0555 Revert "Satisfy clippy"
This reverts commit e8879d5eda.
2023-11-03 00:30:07 -04:00
Christopher Covington
e8879d5eda Satisfy clippy 2023-11-03 00:29:56 -04:00
3081 changed files with 72693 additions and 206721 deletions

View File

@@ -1,3 +1,37 @@
[alias] [alias]
dev = "run --package ruff_dev --bin ruff_dev" dev = "run --package ruff_dev --bin ruff_dev"
benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --" benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
[target.'cfg(all())']
rustflags = [
# CLIPPY LINT SETTINGS
# This is a workaround to configure lints for the entire workspace, pending the ability to configure this via TOML.
# See: `https://github.com/rust-lang/cargo/issues/5034`
# `https://github.com/EmbarkStudios/rust-ecosystem/issues/22#issuecomment-947011395`
"-Dunsafe_code",
"-Wclippy::pedantic",
# Allowed pedantic lints
"-Wclippy::char_lit_as_u8",
"-Aclippy::collapsible_else_if",
"-Aclippy::collapsible_if",
"-Aclippy::implicit_hasher",
"-Aclippy::match_same_arms",
"-Aclippy::missing_errors_doc",
"-Aclippy::missing_panics_doc",
"-Aclippy::module_name_repetitions",
"-Aclippy::must_use_candidate",
"-Aclippy::similar_names",
"-Aclippy::too_many_lines",
# Disallowed restriction lints
"-Wclippy::print_stdout",
"-Wclippy::print_stderr",
"-Wclippy::dbg_macro",
"-Wclippy::empty_drop",
"-Wclippy::empty_structs_with_brackets",
"-Wclippy::exit",
"-Wclippy::get_unwrap",
"-Wclippy::rc_buffer",
"-Wclippy::rc_mutex",
"-Wclippy::rest_pat_in_fully_bound_structs",
"-Wunreachable_pub"
]

View File

@@ -1,8 +0,0 @@
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).
failure-output = "immediate-final"
# Do not cancel the test run on the first failure.
fail-fast = false
status-level = "skip"

5
.gitattributes vendored
View File

@@ -2,11 +2,6 @@
crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_2.py text eol=crlf
crates/ruff_linter/resources/test/fixtures/pycodestyle/W391_3.py text eol=crlf
crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf
crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf
ruff.schema.json linguist-generated=true text=auto eol=lf ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown *.md.snap linguist-language=Markdown

6
.github/CODEOWNERS vendored
View File

@@ -7,9 +7,3 @@
# Jupyter # Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila /crates/ruff_linter/src/jupyter/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood

View File

@@ -3,8 +3,6 @@ Thank you for taking the time to report an issue! We're glad to have you involve
If you're filing a bug report, please consider including the following information: If you're filing a bug report, please consider including the following information:
* List of keywords you searched for before creating this issue. Write them down here so that others can find this issue more easily and help provide feedback.
e.g. "RUF001", "unused variable", "Jupyter notebook"
* A minimal code snippet that reproduces the bug. * A minimal code snippet that reproduces the bug.
* The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag. * The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
* The current Ruff settings (any relevant sections from your `pyproject.toml`). * The current Ruff settings (any relevant sections from your `pyproject.toml`).

View File

@@ -5,14 +5,6 @@ updates:
schedule: schedule:
interval: "weekly" interval: "weekly"
labels: ["internal"] labels: ["internal"]
groups:
actions:
patterns:
- "*"
ignore:
# The latest versions of these are not compatible with our release workflow
- dependency-name: "actions/upload-artifact"
- dependency-name: "actions/download-artifact"
- package-ecosystem: "cargo" - package-ecosystem: "cargo"
directory: "/" directory: "/"

29
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
changelog:
exclude:
labels:
- internal
- documentation
categories:
- title: Breaking Changes
labels:
- breaking
- title: Rules
labels:
- rule
- title: Settings
labels:
- configuration
- cli
- title: Bug Fixes
labels:
- bug
- title: Formatter
labels:
- formatter
- title: Preview
labels:
- preview
- title: Other Changes
labels:
- "*"

View File

@@ -23,19 +23,14 @@ jobs:
name: "Determine changes" name: "Determine changes"
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
# Flag that is raised when any code that affects linter is changed
linter: ${{ steps.changed.outputs.linter_any_changed }} linter: ${{ steps.changed.outputs.linter_any_changed }}
# Flag that is raised when any code that affects formatter is changed
formatter: ${{ steps.changed.outputs.formatter_any_changed }} formatter: ${{ steps.changed.outputs.formatter_any_changed }}
# Flag that is raised when any code is changed
# This is superset of the linter and formatter
code: ${{ steps.changed.outputs.code_any_changed }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: tj-actions/changed-files@v42 - uses: tj-actions/changed-files@v40
id: changed id: changed
with: with:
files_yaml: | files_yaml: |
@@ -48,7 +43,6 @@ jobs:
- "!crates/ruff_dev/**" - "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**" - "!crates/ruff_shrinking/**"
- scripts/* - scripts/*
- python/**
- .github/workflows/ci.yaml - .github/workflows/ci.yaml
formatter: formatter:
@@ -64,19 +58,11 @@ jobs:
- crates/ruff_python_parser/** - crates/ruff_python_parser/**
- crates/ruff_dev/** - crates/ruff_dev/**
- scripts/* - scripts/*
- python/**
- .github/workflows/ci.yaml - .github/workflows/ci.yaml
code:
- "**/*"
- "!**/*.md"
- "!docs/**"
- "!assets/**"
cargo-fmt: cargo-fmt:
name: "cargo fmt" name: "cargo fmt"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -86,9 +72,6 @@ jobs:
cargo-clippy: cargo-clippy:
name: "cargo clippy" name: "cargo clippy"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -97,26 +80,41 @@ jobs:
rustup target add wasm32-unknown-unknown rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: "Clippy" - name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: "Clippy (wasm)" - name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
cargo-test-linux: cargo-test-linux:
name: "cargo test (linux)"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes name: "cargo test (linux)"
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
run: rustup show run: rustup show
- name: "Install mold" - name: "Install cargo insta"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
tool: cargo-nextest tool: cargo-insta
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
run: cargo insta test --all --all-features --unreferenced reject
# 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
with:
name: ruff
path: target/debug/ruff
cargo-test-windows:
runs-on: windows-latest
name: "cargo test (windows)"
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo insta" - name: "Install cargo insta"
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
@@ -124,47 +122,12 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: "Run tests" - name: "Run tests"
shell: bash shell: bash
env: # We can't reject unreferenced snapshots on windows because flake8_executable can't run on windows
NEXTEST_PROFILE: "ci" run: cargo insta test --all --all-features
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# 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@v4
with:
name: ruff
path: target/debug/ruff
cargo-test-windows:
name: "cargo test (windows)"
runs-on: windows-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest
- uses: Swatinem/rust-cache@v2
- name: "Run tests"
shell: bash
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-wasm: cargo-test-wasm:
name: "cargo test (wasm)"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes name: "cargo test (wasm)"
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -182,11 +145,8 @@ jobs:
wasm-pack test --node wasm-pack test --node
cargo-fuzz: cargo-fuzz:
name: "cargo fuzz"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes name: "cargo fuzz"
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -197,15 +157,12 @@ jobs:
- name: "Install cargo-fuzz" - name: "Install cargo-fuzz"
uses: taiki-e/install-action@v2 uses: taiki-e/install-action@v2
with: with:
tool: cargo-fuzz@0.11.2 tool: cargo-fuzz@0.11
- run: cargo fuzz build -s none - run: cargo fuzz build -s none
scripts: scripts:
name: "test scripts" name: "test scripts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -227,30 +184,25 @@ jobs:
- cargo-test-linux - cargo-test-linux
- determine_changes - determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison. # Only runs on pull requests, since that is the only we way we can find the base version for comparison.
# Ecosystem check needs linter and/or formatter changes. if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && ${{
needs.determine_changes.outputs.code == 'true'
}}
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
name: Download comparison Ruff binary name: Download comparison Ruff binary
id: ruff-target id: ruff-target
with: with:
name: ruff name: ruff
path: target/debug path: target/debug
- uses: dawidd6/action-download-artifact@v3 - uses: dawidd6/action-download-artifact@v2
name: Download baseline Ruff binary name: Download baseline Ruff binary
with: with:
name: ruff name: ruff
branch: ${{ github.event.pull_request.base.ref }} branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true check_artifacts: true
- name: Install ruff-ecosystem - name: Install ruff-ecosystem
@@ -325,13 +277,13 @@ jobs:
run: | run: |
echo ${{ github.event.number }} > pr-number echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
name: Upload PR Number name: Upload PR Number
with: with:
name: pr-number name: pr-number
path: pr-number path: pr-number
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
name: Upload Results name: Upload Results
with: with:
name: ecosystem-result name: ecosystem-result
@@ -340,9 +292,6 @@ jobs:
cargo-udeps: cargo-udeps:
name: "cargo udeps" name: "cargo udeps"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install nightly Rust toolchain" - name: "Install nightly Rust toolchain"
@@ -357,10 +306,9 @@ jobs:
python-package: python-package:
name: "python package" name: "python package"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@@ -382,10 +330,9 @@ jobs:
pre-commit: pre-commit:
name: "pre-commit" name: "pre-commit"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -394,7 +341,7 @@ jobs:
- name: "Install pre-commit" - name: "Install pre-commit"
run: pip install pre-commit run: pip install pre-commit
- name: "Cache pre-commit" - name: "Cache pre-commit"
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -411,15 +358,14 @@ jobs:
docs: docs:
name: "mkdocs" name: "mkdocs"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
env: env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
- name: "Add SSH key" - name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.8.0
with: with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -442,14 +388,13 @@ jobs:
run: mkdocs build --strict -f mkdocs.insiders.yml run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs" - name: "Build docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml run: mkdocs build --strict -f mkdocs.generated.yml
check-formatter-instability-and-black-similarity: check-formatter-instability-and-black-similarity:
name: "formatter instabilities and black similarity" name: "formatter instabilities and black similarity"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main' if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -466,13 +411,9 @@ jobs:
check-ruff-lsp: check-ruff-lsp:
name: "test ruff-lsp" name: "test ruff-lsp"
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 needs: cargo-test-linux
needs:
- cargo-test-linux
- determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps: steps:
- uses: extractions/setup-just@v2 - uses: extractions/setup-just@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -481,11 +422,11 @@ jobs:
with: with:
repository: "astral-sh/ruff-lsp" repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
name: Download development ruff binary name: Download development ruff binary
id: ruff-target id: ruff-target
with: with:
@@ -508,9 +449,6 @@ jobs:
benchmarks: benchmarks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps: steps:
- name: "Checkout Branch" - name: "Checkout Branch"
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -529,7 +467,7 @@ jobs:
run: cargo codspeed build --features codspeed -p ruff_benchmark run: cargo codspeed build --features codspeed -p ruff_benchmark
- name: "Run benchmarks" - name: "Run benchmarks"
uses: CodSpeedHQ/action@v2 uses: CodSpeedHQ/action@v1
with: with:
run: cargo codspeed run run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }} token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -20,10 +20,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
- name: "Add SSH key" - name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.8.0
with: with:
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
- name: "Install Rust toolchain" - name: "Install Rust toolchain"
@@ -44,10 +44,10 @@ jobs:
run: mkdocs build --strict -f mkdocs.insiders.yml run: mkdocs build --strict -f mkdocs.insiders.yml
- name: "Build docs" - name: "Build docs"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: mkdocs build --strict -f mkdocs.public.yml run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages" - name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.4.1 uses: cloudflare/wrangler-action@v3.3.2
with: with:
apiToken: ${{ secrets.CF_API_TOKEN }} apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }} accountId: ${{ secrets.CF_ACCOUNT_ID }}

247
.github/workflows/flake8-to-ruff.yaml vendored Normal file
View File

@@ -0,0 +1,247 @@
name: "[flake8-to-ruff] Release"
on: workflow_dispatch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PACKAGE_NAME: flake8-to-ruff
CRATE_NAME: flake8_to_ruff
PYTHON_VERSION: "3.11"
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
jobs:
macos-x86_64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel - x86_64"
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos-universal:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --target universal2-apple-darwin --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel - universal2"
run: |
pip install dist/${{ env.CRATE_NAME }}-*universal2.whl --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel"
shell: bash
run: |
python -m pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, i686]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
linux-cross:
runs-on: ubuntu-latest
strategy:
matrix:
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --no-default-features --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- uses: uraimo/run-on-arch-action@v2
if: matrix.target != 'ppc64'
name: Install built wheel
with:
arch: ${{ matrix.target }}
distro: ubuntu20.04
githubToken: ${{ github.token }}
install: |
apt-get update
apt-get install -y --no-install-recommends python3 python3-pip
pip3 install -U pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
musllinux:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
run: |
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
musllinux-cross:
runs-on: ubuntu-latest
strategy:
matrix:
platform:
- target: aarch64-unknown-linux-musl
arch: aarch64
- target: armv7-unknown-linux-musleabihf
arch: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- uses: uraimo/run-on-arch-action@v2
name: Install built wheel
with:
arch: ${{ matrix.platform.arch }}
distro: alpine_latest
githubToken: ${{ github.token }}
install: |
apk add py3-pip
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
needs:
- macos-universal
- macos-x86_64
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- uses: actions/setup-python@v4
- name: "Publish to PyPi"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.FLAKE8_TO_RUFF_TOKEN }}
run: |
pip install --upgrade twine
twine upload --skip-existing *

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground working-directory: playground
- name: "Deploy to Cloudflare Pages" - name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }} if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.4.1 uses: cloudflare/wrangler-action@v3.3.2
with: with:
apiToken: ${{ secrets.CF_API_TOKEN }} apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }} accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -17,7 +17,7 @@ jobs:
comment: comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dawidd6/action-download-artifact@v3 - uses: dawidd6/action-download-artifact@v2
name: Download pull request number name: Download pull request number
with: with:
name: pr-number name: pr-number
@@ -32,7 +32,7 @@ jobs:
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
fi fi
- uses: dawidd6/action-download-artifact@v3 - uses: dawidd6/action-download-artifact@v2
name: "Download ecosystem results" name: "Download ecosystem results"
id: download-ecosystem-result id: download-ecosystem-result
if: steps.pr-number.outputs.pr-number if: steps.pr-number.outputs.pr-number
@@ -61,7 +61,7 @@ jobs:
echo 'EOF' >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT
- name: Find existing comment - name: Find existing comment
uses: peter-evans/find-comment@v3 uses: peter-evans/find-comment@v2
if: steps.generate-comment.outcome == 'success' if: steps.generate-comment.outcome == 'success'
id: find-comment id: find-comment
with: with:
@@ -71,7 +71,7 @@ jobs:
- name: Create or update comment - name: Create or update comment
if: steps.find-comment.outcome == 'success' if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4 uses: peter-evans/create-or-update-comment@v3
with: with:
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }} issue-number: ${{ steps.pr-number.outputs.pr-number }}

View File

@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"
@@ -52,9 +52,9 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload sdist" - name: "Upload sdist"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-sdist name: wheels
path: dist path: dist
macos-x86_64: macos-x86_64:
@@ -63,7 +63,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@@ -73,26 +73,26 @@ jobs:
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
with: with:
target: x86_64 target: x86_64
args: --release --locked --out dist args: --release --out dist
- name: "Test wheel - x86_64" - name: "Test wheel - x86_64"
run: | run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-macos-x86_64 name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz ARCHIVE_FILE=ruff-x86_64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-macos-x86_64 name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -103,7 +103,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@@ -112,26 +112,26 @@ jobs:
- name: "Build wheels - universal2" - name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
with: with:
args: --release --locked --target universal2-apple-darwin --out dist args: --release --target universal2-apple-darwin --out dist
- name: "Test wheel - universal2" - name: "Test wheel - universal2"
run: | run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-aarch64-apple-darwin name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz ARCHIVE_FILE=ruff-aarch64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-aarch64-apple-darwin name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -151,7 +151,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }} architecture: ${{ matrix.platform.arch }}
@@ -161,7 +161,7 @@ jobs:
uses: PyO3/maturin-action@v1 uses: PyO3/maturin-action@v1
with: with:
target: ${{ matrix.platform.target }} target: ${{ matrix.platform.target }}
args: --release --locked --out dist args: --release --out dist
- name: "Test wheel" - name: "Test wheel"
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash shell: bash
@@ -170,20 +170,20 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
shell: bash shell: bash
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe 7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.zip *.zip
*.sha256 *.sha256
@@ -199,7 +199,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@@ -210,7 +210,7 @@ jobs:
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
manylinux: auto manylinux: auto
args: --release --locked --out dist args: --release --out dist
- name: "Test wheel" - name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }} if: ${{ startsWith(matrix.target, 'x86_64') }}
run: | run: |
@@ -218,19 +218,19 @@ jobs:
ruff --help ruff --help
python -m ruff --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -251,18 +251,14 @@ jobs:
arch: s390x arch: s390x
- target: powerpc64le-unknown-linux-gnu - target: powerpc64le-unknown-linux-gnu
arch: ppc64le arch: ppc64le
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu - target: powerpc64-unknown-linux-gnu
arch: ppc64 arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"
@@ -273,7 +269,7 @@ jobs:
target: ${{ matrix.platform.target }} target: ${{ matrix.platform.target }}
manylinux: auto manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }} docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --release --locked --out dist args: --release --out dist
- uses: uraimo/run-on-arch-action@v2 - uses: uraimo/run-on-arch-action@v2
if: matrix.platform.arch != 'ppc64' if: matrix.platform.arch != 'ppc64'
name: Test wheel name: Test wheel
@@ -289,19 +285,19 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -317,7 +313,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
architecture: x64 architecture: x64
@@ -328,7 +324,7 @@ jobs:
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
manylinux: musllinux_1_2 manylinux: musllinux_1_2
args: --release --locked --out dist args: --release --out dist
- name: "Test wheel" - name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl' if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3 uses: addnab/docker-run-action@v3
@@ -336,24 +332,24 @@ jobs:
image: alpine:latest image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io options: -v ${{ github.workspace }}:/io -w /io
run: | run: |
apk add python3 apk add py3-pip
python -m venv .venv pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ruff --help
.venv/bin/ruff check --help python -m ruff --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -373,7 +369,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ inputs.sha }} ref: ${{ inputs.sha }}
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md" - name: "Prep README.md"
@@ -383,7 +379,7 @@ jobs:
with: with:
target: ${{ matrix.platform.target }} target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2 manylinux: musllinux_1_2
args: --release --locked --out dist args: --release --out dist
docker-options: ${{ matrix.platform.maturin_docker_options }} docker-options: ${{ matrix.platform.maturin_docker_options }}
- uses: uraimo/run-on-arch-action@v2 - uses: uraimo/run-on-arch-action@v2
name: Test wheel name: Test wheel
@@ -392,25 +388,24 @@ jobs:
distro: alpine_latest distro: alpine_latest
githubToken: ${{ github.token }} githubToken: ${{ github.token }}
install: | install: |
apk add python3 apk add py3-pip
run: | run: |
python -m venv .venv pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ruff check --help
.venv/bin/ruff check --help
- name: "Upload wheels" - name: "Upload wheels"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: wheels-${{ matrix.platform.target }} name: wheels
path: dist path: dist
- name: "Archive binary" - name: "Archive binary"
run: | run: |
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary" - name: "Upload binary"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: binaries-${{ matrix.platform.target }} name: binaries
path: | path: |
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -467,11 +462,10 @@ jobs:
# For pypi trusted publishing # For pypi trusted publishing
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
with: with:
pattern: wheels-* name: wheels
path: wheels path: wheels
merge-multiple: true
- name: Publish to PyPi - name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
@@ -511,74 +505,17 @@ jobs:
# For GitHub release publishing # For GitHub release publishing
contents: write contents: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
with: with:
pattern: binaries-* name: binaries
path: binaries path: binaries
merge-multiple: true
- name: "Publish to GitHub" - name: "Publish to GitHub"
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
draft: true draft: true
files: binaries/* files: binaries/*
tag_name: v${{ inputs.tag }} tag_name: v${{ inputs.tag }}
docker-publish:
# This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
# the tag here also
name: Push Docker image ghcr.io/astral-sh/ruff
runs-on: ubuntu-latest
environment:
name: release
permissions:
# For the docker push
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/astral-sh/ruff
- name: Check tag consistency
# Unlike validate-tag we don't check if the commit is on the main branch, but it seems good enough since we can
# change docker tags
if: ${{ inputs.tag }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: "Build and push Docker image"
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
# Reuse the builder
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ inputs.tag != '' }}
tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ inputs.tag || 'dry-run' }}
labels: ${{ steps.meta.outputs.labels }}
# After the release has been published, we update downstream repositories # After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents: update-dependents:
@@ -587,7 +524,7 @@ jobs:
needs: publish-release needs: publish-release
steps: steps:
- name: "Update pre-commit mirror" - name: "Update pre-commit mirror"
uses: actions/github-script@v7 uses: actions/github-script@v6
with: with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }} github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: | script: |

View File

@@ -4,7 +4,7 @@ exclude: |
(?x)^( (?x)^(
crates/ruff_linter/resources/.*| crates/ruff_linter/resources/.*|
crates/ruff_linter/src/rules/.*/snapshots/.*| crates/ruff_linter/src/rules/.*/snapshots/.*|
crates/ruff/resources/.*| crates/ruff_cli/resources/.*|
crates/ruff_python_formatter/resources/.*| crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/tests/snapshots/.*| crates/ruff_python_formatter/tests/snapshots/.*|
crates/ruff_python_resolver/resources/.*| crates/ruff_python_resolver/resources/.*|
@@ -13,12 +13,12 @@ exclude: |
repos: repos:
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.15 rev: v0.12.1
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat - repo: https://github.com/executablebooks/mdformat
rev: 0.7.17 rev: 0.7.16
hooks: hooks:
- id: mdformat - id: mdformat
additional_dependencies: additional_dependencies:
@@ -26,22 +26,16 @@ repos:
- mdformat-admon - mdformat-admon
exclude: | exclude: |
(?x)^( (?x)^(
docs/formatter/black\.md docs/formatter/black.md
| docs/\w+\.md
)$ )$
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.37.0 rev: v0.33.0
hooks: hooks:
- id: markdownlint-fix - id: markdownlint-fix
exclude: |
(?x)^(
docs/formatter/black\.md
| docs/\w+\.md
)$
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: v1.16.22 rev: v1.14.12
hooks: hooks:
- id: typos - id: typos
@@ -55,7 +49,7 @@ repos:
pass_filenames: false # This makes it a lot faster pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.4 rev: v0.1.3
hooks: hooks:
- id: ruff-format - id: ruff-format
- id: ruff - id: ruff
@@ -70,7 +64,7 @@ repos:
# Prettier # Prettier
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3 rev: v3.0.0
hooks: hooks:
- id: prettier - id: prettier
types: [yaml] types: [yaml]

View File

@@ -1,93 +1,5 @@
# Breaking Changes # Breaking Changes
## 0.3.0
### Ruff 2024.2 style
The formatter now formats code according to the Ruff 2024.2 style guide. Read the [changelog](./CHANGELOG.md#030) for a detailed list of stabilized style changes.
### `isort`: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
Previously, Ruff used one or two blank lines (or the number configured by `isort.lines-after-imports`) after imports in typing stub files (`.pyi` files).
The [typing style guide for stubs](https://typing.readthedocs.io/en/latest/source/stubs.html#style-guide) recommends using at most 1 blank line for grouping.
As of this release, `isort` now always uses one blank line after imports in stub files, the same as the formatter.
### `build` is no longer excluded by default ([#10093](https://github.com/astral-sh/ruff/pull/10093))
Ruff maintains a list of directories and files that are excluded by default. This list now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git`
- `.git-rewrite`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `build` directory was included in this list. However, the `build` directory tends to be a not-unpopular directory
name, and excluding it by default caused confusion. Ruff now no longer excludes `build` except if it is excluded by a `.gitignore` file
or because it is listed in `extend-exclude`.
### `--format` is no longer a valid `rule` or `linter` command option
Previously, `ruff rule` and `ruff linter` accepted the `--format <FORMAT>` option as an alias for `--output-format`. Ruff no longer
supports this alias. Please use `ruff rule --output-format <FORMAT>` and `ruff linter --output-format <FORMAT>` instead.
## 0.1.9
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))
Ruff maintains a list of default exclusions, which now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git-rewrite`
- `.git`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `build`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `site-packages` directory was not excluded by default. While `site-packages` tends
to be excluded anyway by virtue of the `.venv` exclusion, this may not be the case when using Ruff
from VS Code outside a virtual environment.
## 0.1.0 ## 0.1.0
### The deprecated `format` setting has been removed ### The deprecated `format` setting has been removed

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
<charlie.r.marsh@gmail.com>. charlie.r.marsh@gmail.com.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@@ -26,10 +26,6 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
- [`cargo dev`](#cargo-dev) - [`cargo dev`](#cargo-dev)
- [Subsystems](#subsystems) - [Subsystems](#subsystems)
- [Compilation Pipeline](#compilation-pipeline) - [Compilation Pipeline](#compilation-pipeline)
- [Import Categorization](#import-categorization)
- [Project root](#project-root)
- [Package root](#package-root)
- [Import categorization](#import-categorization-1)
## The Basics ## The Basics
@@ -39,7 +35,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change. creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.com/invite/astral-sh) to discuss your idea with the You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug) in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted) and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
@@ -67,7 +63,7 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
cargo install cargo-insta cargo install cargo-insta
``` ```
And you'll need pre-commit to run some validation checks: and pre-commit to run some validation checks:
```shell ```shell
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
@@ -80,22 +76,12 @@ when making a commit:
pre-commit install pre-commit install
``` ```
We recommend [nextest](https://nexte.st/) to run Ruff's test suite (via `cargo nextest run`),
though it's not strictly necessary:
```shell
cargo install cargo-nextest --locked
```
Throughout this guide, any usages of `cargo test` can be replaced with `cargo nextest run`,
if you choose to install `nextest`.
### Development ### Development
After cloning the repository, run Ruff locally from the repository root with: After cloning the repository, run Ruff locally from the repository root with:
```shell ```shell
cargo run -p ruff -- check /path/to/file.py --no-cache cargo run -p ruff_cli -- check /path/to/file.py --no-cache
``` ```
Prior to opening a pull request, ensure that your code has been auto-formatted, Prior to opening a pull request, ensure that your code has been auto-formatted,
@@ -134,7 +120,7 @@ At the time of writing, the repository includes the following crates:
If you're working on a rule, this is the crate for you. If you're working on a rule, this is the crate for you.
- `crates/ruff_benchmark`: binary crate for running micro-benchmarks. - `crates/ruff_benchmark`: binary crate for running micro-benchmarks.
- `crates/ruff_cache`: library crate for caching lint results. - `crates/ruff_cache`: library crate for caching lint results.
- `crates/ruff`: binary crate containing Ruff's command-line interface. - `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., - `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`), see the [`cargo dev`](#cargo-dev) section below. `cargo dev generate-all`), see the [`cargo dev`](#cargo-dev) section below.
- `crates/ruff_diagnostics`: library crate for the rule-independent abstractions in the lint - `crates/ruff_diagnostics`: library crate for the rule-independent abstractions in the lint
@@ -245,7 +231,7 @@ Once you've completed the code for the rule itself, you can define tests with th
For example, if you're adding a new rule named `E402`, you would run: For example, if you're adding a new rule named `E402`, you would run:
```shell ```shell
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402 cargo run -p ruff_cli -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402
``` ```
**Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that **Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
@@ -266,7 +252,7 @@ Once you've completed the code for the rule itself, you can define tests with th
Ruff's user-facing settings live in a few different places. Ruff's user-facing settings live in a few different places.
First, the command-line options are defined via the `Args` struct in `crates/ruff/src/args.rs`. First, the command-line options are defined via the `Args` struct in `crates/ruff_cli/src/args.rs`.
Second, the `pyproject.toml` options are defined in `crates/ruff_workspace/src/options.rs` (via the Second, the `pyproject.toml` options are defined in `crates/ruff_workspace/src/options.rs` (via the
`Options` struct), `crates/ruff_workspace/src/configuration.rs` (via the `Configuration` struct), `Options` struct), `crates/ruff_workspace/src/configuration.rs` (via the `Configuration` struct),
@@ -309,14 +295,14 @@ To preview any changes to the documentation locally:
```shell ```shell
# For contributors. # For contributors.
mkdocs serve -f mkdocs.public.yml mkdocs serve -f mkdocs.generated.yml
# For members of the Astral org, which has access to MkDocs Insiders via sponsorship. # For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
mkdocs serve -f mkdocs.insiders.yml mkdocs serve -f mkdocs.insiders.yml
``` ```
The documentation should then be available locally at The documentation should then be available locally at
[http://127.0.0.1:8000/ruff/](http://127.0.0.1:8000/ruff/). [http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
## Release Process ## Release Process
@@ -329,41 +315,23 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
### Creating a new release ### Creating a new release
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh` 1. Update the version with `rg 0.0.269 --files-with-matches | xargs sed -i 's/0.0.269/0.0.270/g'`
1. Run `./scripts/release/bump.sh`; this command will: 1. Update `BREAKING_CHANGES.md`
- Generate a temporary virtual environment with `rooster` 1. Create a PR with the version and `BREAKING_CHANGES.md` updated
- Generate a changelog entry in `CHANGELOG.md`
- Update versions in `pyproject.toml` and `Cargo.toml`
- Update references to versions in the `README.md` and documentation
- Display contributors for the release
1. The changelog should then be editorialized for consistency
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
1. Highlight any breaking changes in `BREAKING_CHANGES.md`
1. Run `cargo check`. This should update the lock file with new versions.
1. Create a pull request with the changelog and version updates
1. Merge the PR 1. Merge the PR
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yaml) with: 1. Run the release workflow with the version number (without starting `v`) as input. Make sure
- The new version number (without starting `v`) main has your merged PR as last commit
- The commit hash of the merged release pull request on `main`
1. The release workflow will do the following: 1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or 1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
uploaded anything, you can restart after pushing a fix. uploaded anything, you can restart after pushing a fix.
1. Upload to PyPI. 1. Upload to PyPI.
1. Create and push the Git tag (as extracted from `pyproject.toml`). We create the Git tag only 1. Create and push the Git tag (as extracted from `pyproject.toml`). We create the Git tag only
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/astral-sh/ruff/issues/4468)). after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/charliermarsh/ruff/issues/4468)).
1. Attach artifacts to draft GitHub release 1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any 1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed. downstream jobs manually if needed.
1. Publish the GitHub release 1. Create release notes in GitHub UI and promote from draft.
1. Open the draft release in the GitHub release section 1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py)
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Append the contributors from the `bump.sh` script
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
1. Once run successfully, you should follow the link in the output to create a PR.
1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories. 1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories.
## Ecosystem CI ## Ecosystem CI
@@ -384,14 +352,9 @@ See the [ruff-ecosystem package](https://github.com/astral-sh/ruff/tree/main/pyt
We have several ways of benchmarking and profiling Ruff: We have several ways of benchmarking and profiling Ruff:
- Our main performance benchmark comparing Ruff with other tools on the CPython codebase - Our main performance benchmark comparing Ruff with other tools on the CPython codebase
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests. - Microbenchmarks which the linter or the formatter on individual files. There run on pull requests.
- Profiling the linter on either the microbenchmarks or entire projects - Profiling the linter on either the microbenchmarks or entire projects
> \[!NOTE\]
> When running benchmarks, ensure that your CPU is otherwise idle (e.g., close any background
> applications, like web browsers). You may also want to switch your CPU to a "performance"
> mode, if it exists, especially when benchmarking short-lived processes.
### CPython Benchmark ### CPython Benchmark
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase, First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
@@ -537,10 +500,10 @@ if the benchmark improved/regressed compared to that baseline.
```shell ```shell
# Run once on your "baseline" code # Run once on your "baseline" code
cargo bench -p ruff_benchmark -- --save-baseline=main cargo benchmark --save-baseline=main
# Then iterate with # Then iterate with
cargo bench -p ruff_benchmark -- --baseline=main cargo benchmark --baseline=main
``` ```
#### PR Summary #### PR Summary
@@ -550,10 +513,10 @@ This is useful to illustrate the improvements of a PR.
```shell ```shell
# On main # On main
cargo bench -p ruff_benchmark -- --save-baseline=main cargo benchmark --save-baseline=main
# After applying your changes # After applying your changes
cargo bench -p ruff_benchmark -- --save-baseline=pr cargo benchmark --save-baseline=pr
critcmp main pr critcmp main pr
``` ```
@@ -566,10 +529,10 @@ cargo install critcmp
#### Tips #### Tips
- Use `cargo bench -p ruff_benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark lexer` - Use `cargo benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic`
to only run the lexer benchmarks. to only run the pydantic tests.
- Use `cargo bench -p ruff_benchmark -- --quiet` for a more cleaned up output (without statistical relevance) - Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance)
- Use `cargo bench -p ruff_benchmark -- --quick` to get faster results (more prone to noise) - Use `cargo benchmark --quick` to get faster results (more prone to noise)
### Profiling Projects ### Profiling Projects
@@ -580,10 +543,10 @@ examples.
#### Linux #### Linux
Install `perf` and build `ruff_benchmark` with the `profiling` profile and then run it with perf Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell ```shell
cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1 cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
``` ```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
@@ -591,8 +554,8 @@ gather enough samples for a good flamegraph (change the 999, the sample rate, an
of checks, to your liking) of checks, to your liking)
```shell ```shell
cargo build --bin ruff_dev --profile=profiling cargo build --bin ruff_dev --profile=release-debug
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
``` ```
Then convert the recorded profile Then convert the recorded profile
@@ -622,7 +585,7 @@ cargo install cargo-instruments
Then run the profiler with Then run the profiler with
```shell ```shell
cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1 cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
``` ```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc` - `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
@@ -637,7 +600,7 @@ Otherwise, follow the instructions from the linux section.
utils with it: utils with it:
- `cargo dev print-ast <file>`: Print the AST of a python file using the - `cargo dev print-ast <file>`: Print the AST of a python file using the
[RustPython parser](https://github.com/astral-sh/ruff/tree/main/crates/ruff_python_parser) that is [RustPython parser](https://github.com/astral-sh/RustPython-Parser/tree/main/parser) that is
mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets mainly used in Ruff. For `if True: pass # comment`, you can see the syntax tree, the byte offsets
for start and stop of each node and also how the `:` token, the comment and whitespace are not for start and stop of each node and also how the `:` token, the comment and whitespace are not
represented anymore: represented anymore:

1432
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,157 +12,51 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT" license = "MIT"
[workspace.dependencies] [workspace.dependencies]
aho-corasick = { version = "1.1.2" } anyhow = { version = "1.0.69" }
annotate-snippets = { version = "0.9.2", features = ["color"] } bitflags = { version = "2.3.1" }
anyhow = { version = "1.0.80" } chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
argfile = { version = "0.1.6" } clap = { version = "4.4.7", features = ["derive"] }
assert_cmd = { version = "2.0.13" } colored = { version = "2.0.0" }
bincode = { version = "1.3.3" } filetime = { version = "0.2.20" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.2", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
colored = { version = "2.1.0" }
configparser = { version = "3.0.3" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.10.1" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version = "2.11.0" }
glob = { version = "0.3.1" } glob = { version = "0.3.1" }
globset = { version = "0.4.14" } globset = { version = "0.4.10" }
hexf-parse = { version = "0.2.1" } ignore = { version = "0.4.20" }
ignore = { version = "0.4.22" } insta = { version = "1.34.0", feature = ["filters", "glob"] }
imara-diff = { version = "0.1.5" } is-macro = { version = "0.3.0" }
imperative = { version = "1.0.4" } itertools = { version = "0.11.0" }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.12.1" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
lalrpop-util = { version = "0.20.0", default-features = false }
lexical-parse-float = { version = "0.8.0", features = ["format"] }
libc = { version = "0.2.153" }
libcst = { version = "1.1.0", default-features = false } libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" } log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" } memchr = { version = "2.6.4" }
lsp-types = { version = "0.95.0", features = ["proposed"] } once_cell = { version = "1.17.1" }
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" }
path-absolutize = { version = "3.1.1" } path-absolutize = { version = "3.1.1" }
pathdiff = { version = "0.2.1" } proc-macro2 = { version = "1.0.69" }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.78" }
pyproject-toml = { version = "0.9.0" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" } quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rayon = { version = "1.8.1" }
regex = { version = "1.10.2" } regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" } rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" } schemars = { version = "0.8.15" }
seahash = { version = "4.1.0" } serde = { version = "1.0.190", features = ["derive"] }
serde = { version = "1.0.197", features = ["derive"] } serde_json = { version = "1.0.107" }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" } shellexpand = { version = "3.0.0" }
shlex = { version = "1.3.0" } similar = { version = "2.3.0", features = ["inline"] }
similar = { version = "2.4.0", features = ["inline"] } smallvec = { version = "1.11.1" }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0" static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] } strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" } strum_macros = { version = "0.25.3" }
syn = { version = "2.0.51" } syn = { version = "2.0.38" }
tempfile = { version = "3.9.0" } test-case = { version = "3.2.1" }
test-case = { version = "3.3.1" } thiserror = { version = "1.0.50" }
thiserror = { version = "1.0.57" } toml = { version = "0.7.8" }
tikv-jemallocator = { version = "0.5.0" }
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" } tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" } tracing-indicatif = { version = "0.3.4" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-tree = { version = "0.2.4" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" } unicode-ident = { version = "1.0.12" }
unicode_names2 = { version = "1.2.0" }
unicode-width = { version = "0.1.11" } unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.2" } uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
ureq = { version = "2.9.6" } wsl = { version = "0.1.0" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.92" }
wasm-bindgen-test = { version = "0.3.40" }
wild = { version = "2" }
[workspace.lints.rust]
unsafe_code = "warn"
unreachable_pub = "warn"
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -2 }
# Allowed pedantic lints
char_lit_as_u8 = "allow"
collapsible_else_if = "allow"
collapsible_if = "allow"
implicit_hasher = "allow"
match_same_arms = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
similar_names = "allow"
too_many_lines = "allow"
# To allow `#[allow(clippy::all)]` in `crates/ruff_python_parser/src/python.rs`.
needless_raw_string_hashes = "allow"
# Disallowed restriction lints
print_stdout = "warn"
print_stderr = "warn"
dbg_macro = "warn"
empty_drop = "warn"
empty_structs_with_brackets = "warn"
exit = "warn"
get_unwrap = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
[profile.release] [profile.release]
# Note that we set these explicitly, and these values lto = "fat"
# were chosen based on a trade-off between compile times
# and runtime performance[1].
#
# [1]: https://github.com/astral-sh/ruff/pull/9031
lto = "thin"
codegen-units = 16
# Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively
# decrease codegen-units in some cases.
[profile.release.package.ruff_python_parser]
codegen-units = 1
[profile.release.package.ruff_python_ast]
codegen-units = 1 codegen-units = 1
[profile.dev.package.insta] [profile.dev.package.insta]
@@ -176,8 +70,8 @@ opt-level = 3
[profile.dev.package.ruff_python_parser] [profile.dev.package.ruff_python_parser]
opt-level = 1 opt-level = 1
# Use the `--profile profiling` flag to show symbols in release mode. # Use the `--profile release-debug` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling` # e.g. `cargo build --profile release-debug`
[profile.profiling] [profile.release-debug]
inherits = "release" inherits = "release"
debug = 1 debug = 1

View File

@@ -1,38 +0,0 @@
FROM --platform=$BUILDPLATFORM ubuntu as build
ENV HOME="/root"
WORKDIR $HOME
RUN apt update && apt install -y build-essential curl python3-venv
# Setup zig as cross compiling linker
RUN python3 -m venv $HOME/.venv
RUN .venv/bin/pip install cargo-zigbuild
ENV PATH="$HOME/.venv/bin:$PATH"
# Install rust
ARG TARGETPLATFORM
RUN case "$TARGETPLATFORM" in \
"linux/arm64") echo "aarch64-unknown-linux-musl" > rust_target.txt ;; \
"linux/amd64") echo "x86_64-unknown-linux-musl" > rust_target.txt ;; \
*) exit 1 ;; \
esac
# Update rustup whenever we bump the rust version
COPY rust-toolchain.toml rust-toolchain.toml
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none
ENV PATH="$HOME/.cargo/bin:$PATH"
# Installs the correct toolchain version from rust-toolchain.toml and then the musl target
RUN rustup target add $(cat rust_target.txt)
# Build
COPY crates crates
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
RUN cargo zigbuild --bin ruff --target $(cat rust_target.txt) --release
RUN cp target/$(cat rust_target.txt)/release/ruff /ruff
# TODO: Optimize binary size, with a version that also works when cross compiling
# RUN strip --strip-all /ruff
FROM scratch
COPY --from=build /ruff /ruff
WORKDIR /io
ENTRYPOINT ["/ruff"]

View File

@@ -7,9 +7,8 @@
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff) [![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff) [![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions) [![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/astral-sh)
[**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/) [**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter and code formatter, written in Rust. An extremely fast Python linter and code formatter, written in Rust.
@@ -55,7 +54,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Pandas](https://github.com/pandas-dev/pandas) - [Pandas](https://github.com/pandas-dev/pandas)
- [SciPy](https://github.com/scipy/scipy) - [SciPy](https://github.com/scipy/scipy)
...and [many more](#whos-using-ruff). ...and many more.
Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster). or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -129,7 +128,7 @@ and with [a variety of other package managers](https://docs.astral.sh/ruff/insta
To run Ruff as a linter, try any of the following: To run Ruff as a linter, try any of the following:
```shell ```shell
ruff check # Lint all files in the current directory (and any subdirectories). ruff check . # Lint all files in the current directory (and any subdirectories).
ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories). ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories).
ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`. ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`.
ruff check path/to/code/to/file.py # Lint `file.py`. ruff check path/to/code/to/file.py # Lint `file.py`.
@@ -139,7 +138,7 @@ ruff check @arguments.txt # Lint using an input file, treating its con
Or, to run Ruff as a formatter: Or, to run Ruff as a formatter:
```shell ```shell
ruff format # Format all files in the current directory (and any subdirectories). ruff format . # Format all files in the current directory (and any subdirectories).
ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories). ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories).
ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`. ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`.
ruff format path/to/code/to/file.py # Format `file.py`. ruff format path/to/code/to/file.py # Format `file.py`.
@@ -149,14 +148,14 @@ ruff format @arguments.txt # Format using an input file, treating its
Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit): Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit):
```yaml ```yaml
# Run the Ruff linter.
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.3.2 rev: v0.1.3
hooks: hooks:
# Run the linter. # Run the Ruff linter.
- id: ruff - id: ruff
args: [ --fix ] # Run the Ruff formatter.
# Run the formatter.
- id: ruff-format - id: ruff-format
``` ```
@@ -173,7 +172,7 @@ jobs:
ruff: ruff:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1 - uses: chartboost/ruff-action@v1
``` ```
@@ -183,9 +182,10 @@ Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml`
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/) [_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
for a complete list of all configuration options). for a complete list of all configuration options).
If left unspecified, Ruff's default configuration is equivalent to the following `ruff.toml` file: If left unspecified, Ruff's default configuration is equivalent to:
```toml ```toml
[tool.ruff]
# Exclude a variety of commonly ignored directories. # Exclude a variety of commonly ignored directories.
exclude = [ exclude = [
".bzr", ".bzr",
@@ -194,25 +194,20 @@ exclude = [
".git", ".git",
".git-rewrite", ".git-rewrite",
".hg", ".hg",
".ipynb_checkpoints",
".mypy_cache", ".mypy_cache",
".nox", ".nox",
".pants.d", ".pants.d",
".pyenv",
".pytest_cache",
".pytype", ".pytype",
".ruff_cache", ".ruff_cache",
".svn", ".svn",
".tox", ".tox",
".venv", ".venv",
".vscode",
"__pypackages__", "__pypackages__",
"_build", "_build",
"buck-out", "buck-out",
"build", "build",
"dist", "dist",
"node_modules", "node_modules",
"site-packages",
"venv", "venv",
] ]
@@ -223,7 +218,7 @@ indent-width = 4
# Assume Python 3.8 # Assume Python 3.8
target-version = "py38" target-version = "py38"
[lint] [tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"] select = ["E4", "E7", "E9", "F"]
ignore = [] ignore = []
@@ -235,7 +230,7 @@ unfixable = []
# Allow unused variables when underscore-prefixed. # Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format] [tool.ruff.format]
# Like Black, use double quotes for strings. # Like Black, use double quotes for strings.
quote-style = "double" quote-style = "double"
@@ -249,20 +244,11 @@ skip-magic-trailing-comma = false
line-ending = "auto" line-ending = "auto"
``` ```
Note that, in a `pyproject.toml`, each section header should be prefixed with `tool.ruff`. For Some configuration options can be provided via the command-line, such as those related to
example, `[lint]` should be replaced with `[tool.ruff.lint]`. rule enablement and disablement, file discovery, and logging level:
Some configuration options can be provided via dedicated command-line arguments, such as those
related to rule enablement and disablement, file discovery, and logging level:
```shell ```shell
ruff check --select F401 --select F403 --quiet ruff check path/to/code/ --select F401 --select F403 --quiet
```
The remaining configuration options can be provided through a catch-all `--config` argument:
```shell
ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}"
``` ```
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format` See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
@@ -350,14 +336,14 @@ For a complete enumeration of the supported rules, see [_Rules_](https://docs.as
Contributions are welcome and highly appreciated. To get started, check out the Contributions are welcome and highly appreciated. To get started, check out the
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/). [**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
You can also join us on [**Discord**](https://discord.com/invite/astral-sh). You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Support ## Support
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues), Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new). or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh). You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Acknowledgements ## Acknowledgements
@@ -387,16 +373,14 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including: Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model)) - Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python)) - Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow) - [Apache Airflow](https://github.com/apache/airflow)
- AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core)) - AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core))
- [Babel](https://github.com/python-babel/babel)
- Benchling ([Refac](https://github.com/benchling/refac)) - Benchling ([Refac](https://github.com/benchling/refac))
- [Babel](https://github.com/python-babel/babel)
- [Bokeh](https://github.com/bokeh/bokeh) - [Bokeh](https://github.com/bokeh/bokeh)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography) - [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- CERN ([Indico](https://getindico.io/))
- [DVC](https://github.com/iterative/dvc) - [DVC](https://github.com/iterative/dvc)
- [Dagger](https://github.com/dagger/dagger) - [Dagger](https://github.com/dagger/dagger)
- [Dagster](https://github.com/dagster-io/dagster) - [Dagster](https://github.com/dagster-io/dagster)
@@ -405,18 +389,15 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Gradio](https://github.com/gradio-app/gradio) - [Gradio](https://github.com/gradio-app/gradio)
- [Great Expectations](https://github.com/great-expectations/great_expectations) - [Great Expectations](https://github.com/great-expectations/great_expectations)
- [HTTPX](https://github.com/encode/httpx) - [HTTPX](https://github.com/encode/httpx)
- [Hatch](https://github.com/pypa/hatch)
- [Home Assistant](https://github.com/home-assistant/core)
- Hugging Face ([Transformers](https://github.com/huggingface/transformers), - Hugging Face ([Transformers](https://github.com/huggingface/transformers),
[Datasets](https://github.com/huggingface/datasets), [Datasets](https://github.com/huggingface/datasets),
[Diffusers](https://github.com/huggingface/diffusers)) [Diffusers](https://github.com/huggingface/diffusers))
- [Hatch](https://github.com/pypa/hatch)
- [Home Assistant](https://github.com/home-assistant/core)
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus)) - ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
- [Ibis](https://github.com/ibis-project/ibis) - [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [Jupyter](https://github.com/jupyter-server/jupyter_server) - [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain) - [LangChain](https://github.com/hwchase17/langchain)
- [Litestar](https://litestar.dev/)
- [LlamaIndex](https://github.com/jerryjliu/llama_index) - [LlamaIndex](https://github.com/jerryjliu/llama_index)
- Matrix ([Synapse](https://github.com/matrix-org/synapse)) - Matrix ([Synapse](https://github.com/matrix-org/synapse))
- [MegaLinter](https://github.com/oxsecurity/megalinter) - [MegaLinter](https://github.com/oxsecurity/megalinter)
@@ -430,36 +411,29 @@ Ruff is used by a number of major open-source projects and companies, including:
- Netflix ([Dispatch](https://github.com/Netflix/dispatch)) - Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon) - [Neon](https://github.com/neondatabase/neon)
- [NoneBot](https://github.com/nonebot/nonebot2) - [NoneBot](https://github.com/nonebot/nonebot2)
- [NumPyro](https://github.com/pyro-ppl/numpyro)
- [ONNX](https://github.com/onnx/onnx) - [ONNX](https://github.com/onnx/onnx)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal) - [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [PDM](https://github.com/pdm-project/pdm) - [PDM](https://github.com/pdm-project/pdm)
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle) - [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
- [Pandas](https://github.com/pandas-dev/pandas) - [Pandas](https://github.com/pandas-dev/pandas)
- [Pillow](https://github.com/python-pillow/Pillow)
- [Poetry](https://github.com/python-poetry/poetry) - [Poetry](https://github.com/python-poetry/poetry)
- [Polars](https://github.com/pola-rs/polars) - [Polars](https://github.com/pola-rs/polars)
- [PostHog](https://github.com/PostHog/posthog) - [PostHog](https://github.com/PostHog/posthog)
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin)) - Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
- [PyInstaller](https://github.com/pyinstaller/pyinstaller) - [PyInstaller](https://github.com/pyinstaller/pyinstaller)
- [PyMC](https://github.com/pymc-devs/pymc/)
- [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing)
- [pytest](https://github.com/pytest-dev/pytest)
- [PyTorch](https://github.com/pytorch/pytorch) - [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic) - [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint) - [Pylint](https://github.com/PyCQA/pylint)
- [PyVista](https://github.com/pyvista/pyvista)
- [Reflex](https://github.com/reflex-dev/reflex) - [Reflex](https://github.com/reflex-dev/reflex)
- [River](https://github.com/online-ml/river)
- [Rippling](https://rippling.com) - [Rippling](https://rippling.com)
- [Robyn](https://github.com/sansyrox/robyn) - [Robyn](https://github.com/sansyrox/robyn)
- [Saleor](https://github.com/saleor/saleor)
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client)) - Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
- [SciPy](https://github.com/scipy/scipy)
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli)) - Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))
- [Saleor](https://github.com/saleor/saleor)
- [SciPy](https://github.com/scipy/scipy)
- [Sphinx](https://github.com/sphinx-doc/sphinx) - [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3) - [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3)
- [Starlette](https://github.com/encode/starlette) - [Litestar](https://litestar.dev/)
- [The Algorithms](https://github.com/TheAlgorithms/Python) - [The Algorithms](https://github.com/TheAlgorithms/Python)
- [Vega-Altair](https://github.com/altair-viz/altair) - [Vega-Altair](https://github.com/altair-viz/altair)
- WordPress ([Openverse](https://github.com/WordPress/openverse)) - WordPress ([Openverse](https://github.com/WordPress/openverse))
@@ -475,7 +449,7 @@ Ruff is used by a number of major open-source projects and companies, including:
### Show Your Support ### Show Your Support
If you're using Ruff, consider adding the Ruff badge to your project's `README.md`: If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
```md ```md
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -501,6 +475,6 @@ MIT
<div align="center"> <div align="center">
<a target="_blank" href="https://astral.sh" style="background:none"> <a target="_blank" href="https://astral.sh" style="background:none">
<img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg" alt="Made by Astral"> <img src="https://raw.githubusercontent.com/astral-sh/ruff/main/assets/svg/Astral.svg">
</a> </a>
</div> </div>

View File

@@ -1,6 +1,5 @@
[files] [files]
# https://github.com/crate-ci/typos/issues/868 extend-exclude = ["resources", "snapshots"]
extend-exclude = ["**/resources/**/*", "**/snapshots/**/*"]
[default.extend-words] [default.extend-words]
hel = "hel" hel = "hel"

View File

@@ -0,0 +1,36 @@
[package]
name = "flake8-to-ruff"
version = "0.1.3"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_linter = { path = "../ruff_linter", default-features = false }
ruff_workspace = { path = "../ruff_workspace" }
anyhow = { workspace = true }
clap = { workspace = true }
colored = { workspace = true }
configparser = { version = "3.0.2" }
itertools = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
pep440_rs = { version = "0.3.12", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
toml = { workspace = true }
[dev-dependencies]
pretty_assertions = "1.3.0"

View File

@@ -0,0 +1,99 @@
# flake8-to-ruff
Convert existing Flake8 configuration files (`setup.cfg`, `tox.ini`, or `.flake8`) for use with
[Ruff](https://github.com/astral-sh/ruff).
Generates a Ruff-compatible `pyproject.toml` section.
## Installation and Usage
### Installation
Available as [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) on PyPI:
```shell
pip install flake8-to-ruff
```
### Usage
To run `flake8-to-ruff`:
```shell
flake8-to-ruff path/to/setup.cfg
flake8-to-ruff path/to/tox.ini
flake8-to-ruff path/to/.flake8
```
`flake8-to-ruff` will print the relevant `pyproject.toml` sections to standard output, like so:
```toml
[tool.ruff]
exclude = [
'.svn',
'CVS',
'.bzr',
'.hg',
'.git',
'__pycache__',
'.tox',
'.idea',
'.mypy_cache',
'.venv',
'node_modules',
'_state_machine.py',
'test_fstring.py',
'bad_coding2.py',
'badsyntax_*.py',
]
select = [
'A',
'E',
'F',
'Q',
]
ignore = []
[tool.ruff.flake8-quotes]
inline-quotes = 'single'
[tool.ruff.pep8-naming]
ignore-names = [
'foo',
'bar',
]
```
### Plugins
`flake8-to-ruff` will attempt to infer any activated plugins based on the settings provided in your
configuration file.
For example, if your `.flake8` file includes a `docstring-convention` property, `flake8-to-ruff`
will enable the appropriate [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
checks.
Alternatively, you can manually specify plugins on the command-line:
```shell
flake8-to-ruff path/to/.flake8 --plugin flake8-builtins --plugin flake8-quotes
```
## Limitations
1. Ruff only supports a subset of the Flake configuration options. `flake8-to-ruff` will warn on and
ignore unsupported options in the `.flake8` file (or equivalent). (Similarly, Ruff has a few
configuration options that don't exist in Flake8.)
1. Ruff will omit any rule codes that are unimplemented or unsupported by Ruff, including rule
codes from unsupported plugins. (See the
[documentation](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) for the complete
list of supported plugins.)
## License
MIT
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md).

View File

@@ -0,0 +1,65 @@
[build-system]
requires = [
# The minimum setuptools version is specific to the PEP 517 backend,
# and may be stricter than the version required in `setup.cfg`
"setuptools>=40.6.0,!=60.9.0",
"wheel",
# Must be kept in sync with the `install_requirements` in `setup.cfg`
"cffi>=1.12; platform_python_implementation != 'PyPy'",
"setuptools-rust>=0.11.4",
]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
target-version = ["py36"]
[tool.pytest.ini_options]
addopts = "-r s --capture=no --strict-markers --benchmark-disable"
markers = [
"skip_fips: this test is not executed in FIPS mode",
"supported: parametrized test requiring only_if and skip_message",
]
[tool.mypy]
show_error_codes = true
check_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unused_configs = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"pretend"
]
ignore_missing_imports = true
[tool.coverage.run]
branch = true
relative_files = true
source = [
"cryptography",
"tests/",
]
[tool.coverage.paths]
source = [
"src/cryptography",
"*.tox/*/lib*/python*/site-packages/cryptography",
"*.tox\\*\\Lib\\site-packages\\cryptography",
"*.tox/pypy/site-packages/cryptography",
]
tests =[
"tests/",
"*tests\\",
]
[tool.coverage.report]
exclude_lines = [
"@abc.abstractmethod",
"@abc.abstractproperty",
"@typing.overload",
"if typing.TYPE_CHECKING",
]

View File

@@ -0,0 +1,91 @@
[metadata]
name = cryptography
version = attr: cryptography.__version__
description = cryptography is a package which provides cryptographic recipes and primitives to Python developers.
long_description = file: README.rst
long_description_content_type = text/x-rst
license = BSD-3-Clause OR Apache-2.0
url = https://github.com/pyca/cryptography
author = The Python Cryptographic Authority and individual contributors
author_email = cryptography-dev@python.org
project_urls =
Documentation=https://cryptography.io/
Source=https://github.com/pyca/cryptography/
Issues=https://github.com/pyca/cryptography/issues
Changelog=https://cryptography.io/en/latest/changelog/
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
License :: OSI Approved :: BSD License
Natural Language :: English
Operating System :: MacOS :: MacOS X
Operating System :: POSIX
Operating System :: POSIX :: BSD
Operating System :: POSIX :: Linux
Operating System :: Microsoft :: Windows
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Security :: Cryptography
[options]
python_requires = >=3.6
include_package_data = True
zip_safe = False
package_dir =
=src
packages = find:
# `install_requires` must be kept in sync with `pyproject.toml`
install_requires =
cffi >=1.12
[options.packages.find]
where = src
exclude =
_cffi_src
_cffi_src.*
[options.extras_require]
test =
pytest>=6.2.0
pytest-benchmark
pytest-cov
pytest-subtests
pytest-xdist
pretend
iso8601
pytz
hypothesis>=1.11.4,!=3.79.2
docs =
sphinx >= 1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0
sphinx_rtd_theme
docstest =
pyenchant >= 1.6.11
twine >= 1.12.0
sphinxcontrib-spelling >= 4.0.1
sdist =
setuptools_rust >= 0.11.4
pep8test =
black
flake8
flake8-import-order
pep8-naming
# This extra is for OpenSSH private keys that use bcrypt KDF
# Versions: v3.1.3 - ignore_few_rounds, v3.1.5 - abi3
ssh =
bcrypt >= 3.1.5
[flake8]
ignore = E203,E211,W503,W504,N818
exclude = .tox,*.egg,.git,_build,.hypothesis
select = E,W,F,N,I
application-import-names = cryptography,cryptography_vectors,tests

View File

@@ -0,0 +1,19 @@
[flake8]
# Ignore style and complexity
# E: style errors
# W: style warnings
# C: complexity
# D: docstring warnings (unused pydocstyle extension)
# F841: local variable assigned but never used
ignore = E, C, W, D, F841
builtins = c, get_config
exclude =
.cache,
.github,
docs,
jupyterhub/alembic*,
onbuild,
scripts,
share,
tools,
setup.py

View File

@@ -0,0 +1,43 @@
[flake8]
# Exclude the grpc generated code
exclude = ./manim/grpc/gen/*
max-complexity = 15
max-line-length = 88
statistics = True
# Prevents some flake8-rst-docstrings errors
rst-roles = attr,class,func,meth,mod,obj,ref,doc,exc
rst-directives = manim, SEEALSO, seealso
docstring-convention=numpy
select = A,A00,B,B9,C4,C90,D,E,F,F,PT,RST,SIM,W
# General Compatibility
extend-ignore = E203, W503, D202, D212, D213, D404
# Misc
F401, F403, F405, F841, E501, E731, E402, F811, F821,
# Plug-in: flake8-builtins
A001, A002, A003,
# Plug-in: flake8-bugbear
B006, B007, B008, B009, B010, B903, B950,
# Plug-in: flake8-simplify
SIM105, SIM106, SIM119,
# Plug-in: flake8-comprehensions
C901
# Plug-in: flake8-pytest-style
PT001, PT004, PT006, PT011, PT018, PT022, PT023,
# Plug-in: flake8-docstrings
D100, D101, D102, D103, D104, D105, D106, D107,
D200, D202, D204, D205, D209,
D301,
D400, D401, D402, D403, D405, D406, D407, D409, D411, D412, D414,
# Plug-in: flake8-rst-docstrings
RST201, RST203, RST210, RST212, RST213, RST215,
RST301, RST303,

View File

@@ -0,0 +1,36 @@
[flake8]
min_python_version = 3.7.0
max-line-length = 88
ban-relative-imports = true
# flake8-use-fstring: https://github.com/MichaelKim0407/flake8-use-fstring#--percent-greedy-and---format-greedy
format-greedy = 1
inline-quotes = double
enable-extensions = TC, TC1
type-checking-strict = true
eradicate-whitelist-extend = ^-.*;
extend-ignore =
# E203: Whitespace before ':' (pycqa/pycodestyle#373)
E203,
# SIM106: Handle error-cases first
SIM106,
# ANN101: Missing type annotation for self in method
ANN101,
# ANN102: Missing type annotation for cls in classmethod
ANN102,
# PIE781: assign-and-return
PIE781,
# PIE798 no-unnecessary-class: Consider using a module for namespacing instead
PIE798,
per-file-ignores =
# TC002: Move third-party import '...' into a type-checking block
__init__.py:TC002,
# ANN201: Missing return type annotation for public function
tests/test_*:ANN201
tests/**/test_*:ANN201
extend-exclude =
# Frozen and not subject to change in this repo:
get-poetry.py,
install-poetry.py,
# External to the project's coding standards:
tests/fixtures/*,
tests/**/fixtures/*,

View File

@@ -0,0 +1,19 @@
[flake8]
max-line-length=120
docstring-convention=all
import-order-style=pycharm
application_import_names=bot,tests
exclude=.cache,.venv,.git,constants.py
extend-ignore=
B311,W503,E226,S311,T000,E731
# Missing Docstrings
D100,D104,D105,D107,
# Docstring Whitespace
D203,D212,D214,D215,
# Docstring Quotes
D301,D302,
# Docstring Content
D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417
# Type Annotations
ANN002,ANN003,ANN101,ANN102,ANN204,ANN206,ANN401
per-file-ignores=tests/*:D,ANN

View File

@@ -0,0 +1,6 @@
[flake8]
ignore = E203, E501, W503
per-file-ignores =
requests/__init__.py:E402, F401
requests/compat.py:E402, F401
tests/compat.py:F401

View File

@@ -0,0 +1,34 @@
[project]
name = "flake8-to-ruff"
keywords = ["automation", "flake8", "pycodestyle", "pyflakes", "pylint", "clippy"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
]
author = "Charlie Marsh"
author_email = "charlie.r.marsh@gmail.com"
description = "Convert existing Flake8 configuration to Ruff."
requires-python = ">=3.7"
[project.urls]
repository = "https://github.com/astral-sh/ruff#subdirectory=crates/flake8_to_ruff"
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]
bindings = "bin"
strip = true

View File

@@ -0,0 +1,13 @@
//! Extract Black configuration settings from a pyproject.toml.
use ruff_linter::line_width::LineLength;
use ruff_linter::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Black {
#[serde(alias = "line-length", alias = "line_length")]
pub(crate) line_length: Option<LineLength>,
#[serde(alias = "target-version", alias = "target_version")]
pub(crate) target_version: Option<Vec<PythonVersion>>,
}

View File

@@ -0,0 +1,687 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use itertools::Itertools;
use ruff_linter::line_width::LineLength;
use ruff_linter::registry::Linter;
use ruff_linter::rule_selector::RuleSelector;
use ruff_linter::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::rules::flake8_tidy_imports::settings::Strictness;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::settings::types::PythonVersion;
use ruff_linter::settings::DEFAULT_SELECTORS;
use ruff_linter::warn_user;
use ruff_workspace::options::{
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintCommonOptions,
LintOptions, McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
};
use ruff_workspace::pyproject::Pyproject;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
pub(crate) fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
external_config: &ExternalConfig,
plugins: Option<Vec<Plugin>>,
) -> Pyproject {
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file");
// Extract all referenced rule code prefixes, to power plugin inference.
let mut referenced_codes: HashSet<RuleSelector> = HashSet::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
"select" | "ignore" | "extend-select" | "extend_select" | "extend-ignore"
| "extend_ignore" => {
referenced_codes.extend(parser::parse_prefix_codes(value.as_ref()));
}
"per-file-ignores" | "per_file_ignores" => {
if let Ok(per_file_ignores) =
parser::parse_files_to_codes_mapping(value.as_ref())
{
for (_, codes) in parser::collect_per_file_ignores(per_file_ignores) {
referenced_codes.extend(codes);
}
}
}
_ => {}
}
}
}
// Infer plugins, if not provided.
let plugins = plugins.unwrap_or_else(|| {
let from_options = plugin::infer_plugins_from_options(flake8);
if !from_options.is_empty() {
#[allow(clippy::print_stderr)]
{
eprintln!("Inferred plugins from settings: {from_options:#?}");
}
}
let from_codes = plugin::infer_plugins_from_codes(&referenced_codes);
if !from_codes.is_empty() {
#[allow(clippy::print_stderr)]
{
eprintln!("Inferred plugins from referenced codes: {from_codes:#?}");
}
}
from_options.into_iter().chain(from_codes).collect()
});
// Check if the user has specified a `select`. If not, we'll add our own
// default `select`, and populate it based on user plugins.
let mut select = flake8
.get("select")
.and_then(|value| {
value
.as_ref()
.map(|value| HashSet::from_iter(parser::parse_prefix_codes(value)))
})
.unwrap_or_else(|| resolve_select(&plugins));
let mut ignore: HashSet<RuleSelector> = flake8
.get("ignore")
.and_then(|value| {
value
.as_ref()
.map(|value| HashSet::from_iter(parser::parse_prefix_codes(value)))
})
.unwrap_or_default();
// Parse each supported option.
let mut options = Options::default();
let mut lint_options = LintCommonOptions::default();
let mut flake8_annotations = Flake8AnnotationsOptions::default();
let mut flake8_bugbear = Flake8BugbearOptions::default();
let mut flake8_builtins = Flake8BuiltinsOptions::default();
let mut flake8_errmsg = Flake8ErrMsgOptions::default();
let mut flake8_pytest_style = Flake8PytestStyleOptions::default();
let mut flake8_quotes = Flake8QuotesOptions::default();
let mut flake8_tidy_imports = Flake8TidyImportsOptions::default();
let mut mccabe = McCabeOptions::default();
let mut pep8_naming = Pep8NamingOptions::default();
let mut pydocstyle = PydocstyleOptions::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
// flake8
"builtins" => {
options.builtins = Some(parser::parse_strings(value.as_ref()));
}
"max-line-length" | "max_line_length" => match LineLength::from_str(value) {
Ok(line_length) => options.line_length = Some(line_length),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
},
"select" => {
// No-op (handled above).
select.extend(parser::parse_prefix_codes(value.as_ref()));
}
"ignore" => {
// No-op (handled above).
}
"extend-select" | "extend_select" => {
// Unlike Flake8, use a single explicit `select`.
select.extend(parser::parse_prefix_codes(value.as_ref()));
}
"extend-ignore" | "extend_ignore" => {
// Unlike Flake8, use a single explicit `ignore`.
ignore.extend(parser::parse_prefix_codes(value.as_ref()));
}
"exclude" => {
options.exclude = Some(parser::parse_strings(value.as_ref()));
}
"extend-exclude" | "extend_exclude" => {
options.extend_exclude = Some(parser::parse_strings(value.as_ref()));
}
"per-file-ignores" | "per_file_ignores" => {
match parser::parse_files_to_codes_mapping(value.as_ref()) {
Ok(per_file_ignores) => {
lint_options.per_file_ignores =
Some(parser::collect_per_file_ignores(per_file_ignores));
}
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
// flake8-bugbear
"extend-immutable-calls" | "extend_immutable_calls" => {
flake8_bugbear.extend_immutable_calls =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-builtins
"builtins-ignorelist" | "builtins_ignorelist" => {
flake8_builtins.builtins_ignorelist =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.suppress_none_returning = Some(bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
"suppress-dummy-args" | "suppress_dummy_args" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.suppress_dummy_args = Some(bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
"mypy-init-return" | "mypy_init_return" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.mypy_init_return = Some(bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
"allow-star-arg-any" | "allow_star_arg_any" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_annotations.allow_star_arg_any = Some(bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
// flake8-quotes
"quotes" | "inline-quotes" | "inline_quotes" => match value.trim() {
"'" | "single" => flake8_quotes.inline_quotes = Some(Quote::Single),
"\"" | "double" => flake8_quotes.inline_quotes = Some(Quote::Double),
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
},
"multiline-quotes" | "multiline_quotes" => match value.trim() {
"'" | "single" => flake8_quotes.multiline_quotes = Some(Quote::Single),
"\"" | "double" => flake8_quotes.multiline_quotes = Some(Quote::Double),
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
},
"docstring-quotes" | "docstring_quotes" => match value.trim() {
"'" | "single" => flake8_quotes.docstring_quotes = Some(Quote::Single),
"\"" | "double" => flake8_quotes.docstring_quotes = Some(Quote::Double),
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
},
"avoid-escape" | "avoid_escape" => match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_quotes.avoid_escape = Some(bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
},
// pep8-naming
"ignore-names" | "ignore_names" => {
pep8_naming.ignore_names = Some(parser::parse_strings(value.as_ref()));
}
"classmethod-decorators" | "classmethod_decorators" => {
pep8_naming.classmethod_decorators =
Some(parser::parse_strings(value.as_ref()));
}
"staticmethod-decorators" | "staticmethod_decorators" => {
pep8_naming.staticmethod_decorators =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-tidy-imports
"ban-relative-imports" | "ban_relative_imports" => match value.trim() {
"true" => flake8_tidy_imports.ban_relative_imports = Some(Strictness::All),
"parents" => {
flake8_tidy_imports.ban_relative_imports = Some(Strictness::Parents);
}
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
},
// flake8-docstrings
"docstring-convention" => match value.trim() {
"google" => pydocstyle.convention = Some(Convention::Google),
"numpy" => pydocstyle.convention = Some(Convention::Numpy),
"pep257" => pydocstyle.convention = Some(Convention::Pep257),
"all" => pydocstyle.convention = None,
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
},
// mccabe
"max-complexity" | "max_complexity" => match value.parse::<usize>() {
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
},
// flake8-errmsg
"errmsg-max-string-length" | "errmsg_max_string_length" => {
match value.parse::<usize>() {
Ok(max_string_length) => {
flake8_errmsg.max_string_length = Some(max_string_length);
}
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
// flake8-pytest-style
"pytest-fixture-no-parentheses" | "pytest_fixture_no_parentheses " => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_pytest_style.fixture_parentheses = Some(!bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
"pytest-parametrize-names-type" | "pytest_parametrize_names_type" => {
match value.trim() {
"csv" => {
flake8_pytest_style.parametrize_names_type =
Some(ParametrizeNameType::Csv);
}
"tuple" => {
flake8_pytest_style.parametrize_names_type =
Some(ParametrizeNameType::Tuple);
}
"list" => {
flake8_pytest_style.parametrize_names_type =
Some(ParametrizeNameType::List);
}
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
}
}
"pytest-parametrize-values-type" | "pytest_parametrize_values_type" => {
match value.trim() {
"tuple" => {
flake8_pytest_style.parametrize_values_type =
Some(ParametrizeValuesType::Tuple);
}
"list" => {
flake8_pytest_style.parametrize_values_type =
Some(ParametrizeValuesType::List);
}
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
}
}
"pytest-parametrize-values-row-type" | "pytest_parametrize_values_row_type" => {
match value.trim() {
"tuple" => {
flake8_pytest_style.parametrize_values_row_type =
Some(ParametrizeValuesRowType::Tuple);
}
"list" => {
flake8_pytest_style.parametrize_values_row_type =
Some(ParametrizeValuesRowType::List);
}
_ => {
warn_user!("Unexpected '{key}' value: {value}");
}
}
}
"pytest-raises-require-match-for" | "pytest_raises_require_match_for" => {
flake8_pytest_style.raises_require_match_for =
Some(parser::parse_strings(value.as_ref()));
}
"pytest-mark-no-parentheses" | "pytest_mark_no_parentheses" => {
match parser::parse_bool(value.as_ref()) {
Ok(bool) => flake8_pytest_style.mark_parentheses = Some(!bool),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
}
}
}
// Unknown
_ => {
warn_user!("Skipping unsupported property: {}", key);
}
}
}
}
// Deduplicate and sort.
lint_options.select = Some(
select
.into_iter()
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
lint_options.ignore = Some(
ignore
.into_iter()
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
if flake8_annotations != Flake8AnnotationsOptions::default() {
lint_options.flake8_annotations = Some(flake8_annotations);
}
if flake8_bugbear != Flake8BugbearOptions::default() {
lint_options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_builtins != Flake8BuiltinsOptions::default() {
lint_options.flake8_builtins = Some(flake8_builtins);
}
if flake8_errmsg != Flake8ErrMsgOptions::default() {
lint_options.flake8_errmsg = Some(flake8_errmsg);
}
if flake8_pytest_style != Flake8PytestStyleOptions::default() {
lint_options.flake8_pytest_style = Some(flake8_pytest_style);
}
if flake8_quotes != Flake8QuotesOptions::default() {
lint_options.flake8_quotes = Some(flake8_quotes);
}
if flake8_tidy_imports != Flake8TidyImportsOptions::default() {
lint_options.flake8_tidy_imports = Some(flake8_tidy_imports);
}
if mccabe != McCabeOptions::default() {
lint_options.mccabe = Some(mccabe);
}
if pep8_naming != Pep8NamingOptions::default() {
lint_options.pep8_naming = Some(pep8_naming);
}
if pydocstyle != PydocstyleOptions::default() {
lint_options.pydocstyle = Some(pydocstyle);
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = &external_config.black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
}
if let Some(target_version) = &black.target_version {
if let Some(target_version) = target_version.iter().min() {
options.target_version = Some(*target_version);
}
}
}
if let Some(isort) = &external_config.isort {
if let Some(src_paths) = &isort.src_paths {
match options.src.as_mut() {
Some(src) => {
src.extend_from_slice(src_paths);
}
None => {
options.src = Some(src_paths.clone());
}
}
}
}
if let Some(project) = &external_config.project {
if let Some(requires_python) = &project.requires_python {
if options.target_version.is_none() {
options.target_version =
PythonVersion::get_minimum_supported_version(requires_python);
}
}
}
if lint_options != LintCommonOptions::default() {
options.lint = Some(LintOptions {
common: lint_options,
..LintOptions::default()
});
}
// Create the pyproject.toml.
Pyproject::new(options)
}
/// Resolve the set of enabled `RuleSelector` values for the given
/// plugins.
fn resolve_select(plugins: &[Plugin]) -> HashSet<RuleSelector> {
let mut select: HashSet<_> = DEFAULT_SELECTORS.iter().cloned().collect();
select.extend(plugins.iter().map(|p| Linter::from(p).into()));
select
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::str::FromStr;
use anyhow::Result;
use itertools::Itertools;
use pep440_rs::VersionSpecifiers;
use pretty_assertions::assert_eq;
use ruff_linter::line_width::LineLength;
use ruff_linter::registry::Linter;
use ruff_linter::rule_selector::RuleSelector;
use ruff_linter::rules::flake8_quotes;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::settings::types::PythonVersion;
use ruff_workspace::options::{
Flake8QuotesOptions, LintCommonOptions, LintOptions, Options, PydocstyleOptions,
};
use ruff_workspace::pyproject::Pyproject;
use crate::converter::DEFAULT_SELECTORS;
use crate::pep621::Project;
use crate::ExternalConfig;
use super::super::plugin::Plugin;
use super::convert;
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintCommonOptions {
LintCommonOptions {
ignore: Some(vec![]),
select: Some(
DEFAULT_SELECTORS
.iter()
.cloned()
.chain(plugins)
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
),
..LintCommonOptions::default()
}
}
#[test]
fn it_converts_empty() {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
&ExternalConfig::default(),
None,
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_converts_dashes() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
)]),
&ExternalConfig::default(),
Some(vec![]),
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_converts_underscores() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
)]),
&ExternalConfig::default(),
Some(vec![]),
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_ignores_parse_errors() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
)]),
&ExternalConfig::default(),
Some(vec![]),
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_converts_plugin_options() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
&ExternalConfig::default(),
Some(vec![]),
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
common: LintCommonOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([])
},
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_converts_docstring_conventions() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([(
"docstring-convention".to_string(),
Some("numpy".to_string()),
)]),
)]),
&ExternalConfig::default(),
Some(vec![Plugin::Flake8Docstrings]),
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
common: LintCommonOptions {
pydocstyle: Some(PydocstyleOptions {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
}),
..lint_default_options([Linter::Pydocstyle.into()])
},
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_infers_plugins_if_omitted() {
let actual = convert(
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
&ExternalConfig::default(),
None,
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
common: LintCommonOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([Linter::Flake8Quotes.into()])
},
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
}
#[test]
fn it_converts_project_requires_python() -> Result<()> {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
&ExternalConfig {
project: Some(&Project {
requires_python: Some(VersionSpecifiers::from_str(">=3.8.16, <3.11")?),
}),
..ExternalConfig::default()
},
Some(vec![]),
);
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;
#[derive(Default)]
pub(crate) struct ExternalConfig<'a> {
pub(crate) black: Option<&'a Black>,
pub(crate) isort: Option<&'a Isort>,
pub(crate) project: Option<&'a Project>,
}

View File

@@ -0,0 +1,10 @@
//! Extract isort configuration settings from a pyproject.toml.
use serde::{Deserialize, Serialize};
/// The [isort configuration](https://pycqa.github.io/isort/docs/configuration/config_files.html).
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Isort {
#[serde(alias = "src-paths", alias = "src_paths")]
pub(crate) src_paths: Option<Vec<String>>,
}

View File

@@ -0,0 +1,80 @@
//! Utility to generate Ruff's `pyproject.toml` section from a Flake8 INI file.
mod black;
mod converter;
mod external_config;
mod isort;
mod parser;
mod pep621;
mod plugin;
mod pyproject;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use crate::converter::convert;
use crate::external_config::ExternalConfig;
use crate::plugin::Plugin;
use crate::pyproject::parse;
use ruff_linter::logging::{set_up_logging, LogLevel};
#[derive(Parser)]
#[command(
about = "Convert existing Flake8 configuration to Ruff.",
long_about = None
)]
struct Args {
/// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or
/// `.flake8`).
#[arg(required = true)]
file: PathBuf,
/// Optional path to a `pyproject.toml` file, used to ensure compatibility
/// with Black.
#[arg(long)]
pyproject: Option<PathBuf>,
/// List of plugins to enable.
#[arg(long, value_delimiter = ',')]
plugin: Option<Vec<Plugin>>,
}
fn main() -> Result<()> {
set_up_logging(&LogLevel::Default)?;
let args = Args::parse();
// Read the INI file.
let mut ini = Ini::new_cs();
ini.set_multiline(true);
let config = ini.load(args.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let pyproject = args.pyproject.map(parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
.map(|tool| ExternalConfig {
black: tool.black.as_ref(),
isort: tool.isort.as_ref(),
..Default::default()
})
.unwrap_or_default();
let external_config = ExternalConfig {
project: pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
..external_config
};
// Create Ruff's pyproject.toml section.
let pyproject = convert(&config, &external_config, args.plugin);
#[allow(clippy::print_stdout)]
{
println!("{}", toml::to_string_pretty(&pyproject)?);
}
Ok(())
}

View File

@@ -0,0 +1,391 @@
use std::str::FromStr;
use anyhow::{bail, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashMap;
use ruff_linter::settings::types::PatternPrefixPair;
use ruff_linter::{warn_user, RuleSelector};
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
/// Parse a comma-separated list of `RuleSelector` values (e.g.,
/// "F401,E501").
pub(crate) fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
let mut codes: Vec<RuleSelector> = vec![];
for code in COMMA_SEPARATED_LIST_RE.split(value) {
let code = code.trim();
if code.is_empty() {
continue;
}
if let Ok(code) = RuleSelector::from_str(code) {
codes.push(code);
} else {
warn_user!("Unsupported prefix code: {code}");
}
}
codes
}
/// Parse a comma-separated list of strings (e.g., "__init__.py,__main__.py").
pub(crate) fn parse_strings(value: &str) -> Vec<String> {
COMMA_SEPARATED_LIST_RE
.split(value)
.map(str::trim)
.filter(|part| !part.is_empty())
.map(String::from)
.collect()
}
/// Parse a boolean.
pub(crate) fn parse_bool(value: &str) -> Result<bool> {
match value.trim() {
"true" => Ok(true),
"false" => Ok(false),
_ => bail!("Unexpected boolean value: {value}"),
}
}
#[derive(Debug)]
struct Token {
token_name: TokenType,
src: String,
}
#[derive(Debug, Copy, Clone)]
enum TokenType {
Code,
File,
Colon,
Comma,
Ws,
Eof,
}
struct State {
seen_sep: bool,
seen_colon: bool,
filenames: Vec<String>,
codes: Vec<String>,
}
impl State {
const fn new() -> Self {
Self {
seen_sep: true,
seen_colon: false,
filenames: vec![],
codes: vec![],
}
}
/// Generate the list of `StrRuleCodePair` pairs for the current
/// state.
fn parse(&self) -> Vec<PatternPrefixPair> {
let mut codes: Vec<PatternPrefixPair> = vec![];
for code in &self.codes {
if let Ok(code) = RuleSelector::from_str(code) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
} else {
warn_user!("Unsupported prefix code: {code}");
}
}
codes
}
}
/// Tokenize the raw 'files-to-codes' mapping.
fn tokenize_files_to_codes_mapping(value: &str) -> Vec<Token> {
let mut tokens = vec![];
let mut i = 0;
while i < value.len() {
for (token_re, token_name) in [
(
Regex::new(r"([A-Z]+[0-9]*)(?:$|\s|,)").unwrap(),
TokenType::Code,
),
(Regex::new(r"([^\s:,]+)").unwrap(), TokenType::File),
(Regex::new(r"(\s*:\s*)").unwrap(), TokenType::Colon),
(Regex::new(r"(\s*,\s*)").unwrap(), TokenType::Comma),
(Regex::new(r"(\s+)").unwrap(), TokenType::Ws),
] {
if let Some(cap) = token_re.captures(&value[i..]) {
let mat = cap.get(1).unwrap();
if mat.start() == 0 {
tokens.push(Token {
token_name,
src: mat.as_str().trim().to_string(),
});
i += mat.end();
break;
}
}
}
}
tokens.push(Token {
token_name: TokenType::Eof,
src: String::new(),
});
tokens
}
/// Parse a 'files-to-codes' mapping, mimicking Flake8's internal logic.
/// See: <https://github.com/PyCQA/flake8/blob/7dfe99616fc2f07c0017df2ba5fa884158f3ea8a/src/flake8/utils.py#L45>
pub(crate) fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair>> {
if value.trim().is_empty() {
return Ok(vec![]);
}
let mut codes: Vec<PatternPrefixPair> = vec![];
let mut state = State::new();
for token in tokenize_files_to_codes_mapping(value) {
if matches!(token.token_name, TokenType::Comma | TokenType::Ws) {
state.seen_sep = true;
} else if !state.seen_colon {
if matches!(token.token_name, TokenType::Colon) {
state.seen_colon = true;
state.seen_sep = true;
} else if state.seen_sep && matches!(token.token_name, TokenType::File) {
state.filenames.push(token.src);
state.seen_sep = false;
} else {
bail!("Unexpected token: {:?}", token.token_name);
}
} else {
if matches!(token.token_name, TokenType::Eof) {
codes.extend(state.parse());
state = State::new();
} else if state.seen_sep && matches!(token.token_name, TokenType::Code) {
state.codes.push(token.src);
state.seen_sep = false;
} else if state.seen_sep && matches!(token.token_name, TokenType::File) {
codes.extend(state.parse());
state = State::new();
state.filenames.push(token.src);
state.seen_sep = false;
} else {
bail!("Unexpected token: {:?}", token.token_name);
}
}
}
Ok(codes)
}
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
pub(crate) fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
) -> FxHashMap<String, Vec<RuleSelector>> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)
.or_default()
.push(pair.prefix);
}
per_file_ignores
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_linter::codes;
use ruff_linter::registry::Linter;
use ruff_linter::settings::types::PatternPrefixPair;
use ruff_linter::RuleSelector;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
#[test]
fn it_parses_prefix_codes() {
let actual = parse_prefix_codes("");
let expected: Vec<RuleSelector> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes(" ");
let expected: Vec<RuleSelector> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401");
let expected = vec![codes::Pyflakes::_401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,");
let expected = vec![codes::Pyflakes::_401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,E501");
let expected = vec![
codes::Pyflakes::_401.into(),
codes::Pycodestyle::E501.into(),
];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401, E501");
let expected = vec![
codes::Pyflakes::_401.into(),
codes::Pycodestyle::E501.into(),
];
assert_eq!(actual, expected);
}
#[test]
fn it_parses_strings() {
let actual = parse_strings("");
let expected: Vec<String> = vec![];
assert_eq!(actual, expected);
let actual = parse_strings(" ");
let expected: Vec<String> = vec![];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py");
let expected = vec!["__init__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py,");
let expected = vec!["__init__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py,__main__.py");
let expected = vec!["__init__.py".to_string(), "__main__.py".to_string()];
assert_eq!(actual, expected);
let actual = parse_strings("__init__.py, __main__.py");
let expected = vec!["__init__.py".to_string(), "__main__.py".to_string()];
assert_eq!(actual, expected);
}
#[test]
fn it_parse_files_to_codes_mapping() -> Result<()> {
let actual = parse_files_to_codes_mapping("")?;
let expected: Vec<PatternPrefixPair> = vec![];
assert_eq!(actual, expected);
let actual = parse_files_to_codes_mapping(" ")?;
let expected: Vec<PatternPrefixPair> = vec![];
assert_eq!(actual, expected);
// Ex) locust
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
locust/test/*: F841
examples/*: F841
*.pyi: E302,E704"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "locust/test/*".to_string(),
prefix: codes::Pyflakes::_841.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: codes::Pyflakes::_841.into(),
},
];
assert_eq!(actual, expected);
// Ex) celery
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
t/*,setup.py,examples/*,docs/*,extra/*:
D,"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "t/*".to_string(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "setup.py".to_string(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "docs/*".to_string(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "extra/*".to_string(),
prefix: Linter::Pydocstyle.into(),
},
];
assert_eq!(actual, expected);
// Ex) scrapy
let actual = parse_files_to_codes_mapping(
"per-file-ignores =
scrapy/__init__.py:E402
scrapy/core/downloader/handlers/http.py:F401
scrapy/http/__init__.py:F401
scrapy/linkextractors/__init__.py:E402,F401
scrapy/selector/__init__.py:F401
scrapy/spiders/__init__.py:E402,F401
scrapy/utils/url.py:F403,F405
tests/test_loader.py:E741"
.strip_prefix("per-file-ignores =")
.unwrap(),
)?;
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "scrapy/__init__.py".to_string(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/http/__init__.py".to_string(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/selector/__init__.py".to_string(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: codes::Pyflakes::_403.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: codes::Pyflakes::_405.into(),
},
PatternPrefixPair {
pattern: "tests/test_loader.py".to_string(),
prefix: codes::Pycodestyle::E741.into(),
},
];
assert_eq!(actual, expected);
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
//! Extract PEP 621 configuration settings from a pyproject.toml.
use pep440_rs::VersionSpecifiers;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Project {
#[serde(alias = "requires-python", alias = "requires_python")]
pub(crate) requires_python: Option<VersionSpecifiers>,
}

View File

@@ -0,0 +1,368 @@
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use ruff_linter::registry::Linter;
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::RuleSelector;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
Flake82020,
Flake8Annotations,
Flake8Bandit,
Flake8BlindExcept,
Flake8BooleanTrap,
Flake8Bugbear,
Flake8Builtins,
Flake8Commas,
Flake8Comprehensions,
Flake8Datetimez,
Flake8Debugger,
Flake8Docstrings,
Flake8Eradicate,
Flake8ErrMsg,
Flake8Executable,
Flake8ImplicitStrConcat,
Flake8ImportConventions,
Flake8NoPep420,
Flake8Pie,
Flake8Print,
Flake8PytestStyle,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
Flake8TypeChecking,
Flake8UnusedArguments,
Flake8UsePathlib,
McCabe,
PEP8Naming,
PandasVet,
Pyupgrade,
Tryceratops,
}
impl FromStr for Plugin {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"flake8-2020" => Ok(Plugin::Flake82020),
"flake8-annotations" => Ok(Plugin::Flake8Annotations),
"flake8-bandit" => Ok(Plugin::Flake8Bandit),
"flake8-blind-except" => Ok(Plugin::Flake8BlindExcept),
"flake8-boolean-trap" => Ok(Plugin::Flake8BooleanTrap),
"flake8-bugbear" => Ok(Plugin::Flake8Bugbear),
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-commas" => Ok(Plugin::Flake8Commas),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
"flake8-datetimez" => Ok(Plugin::Flake8Datetimez),
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8Eradicate),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-executable" => Ok(Plugin::Flake8Executable),
"flake8-implicit-str-concat" => Ok(Plugin::Flake8ImplicitStrConcat),
"flake8-import-conventions" => Ok(Plugin::Flake8ImportConventions),
"flake8-no-pep420" => Ok(Plugin::Flake8NoPep420),
"flake8-pie" => Ok(Plugin::Flake8Pie),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-pytest-style" => Ok(Plugin::Flake8PytestStyle),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
"flake8-simplify" => Ok(Plugin::Flake8Simplify),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"flake8-type-checking" => Ok(Plugin::Flake8TypeChecking),
"flake8-unused-arguments" => Ok(Plugin::Flake8UnusedArguments),
"flake8-use-pathlib" => Ok(Plugin::Flake8UsePathlib),
"mccabe" => Ok(Plugin::McCabe),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pandas-vet" => Ok(Plugin::PandasVet),
"pyupgrade" => Ok(Plugin::Pyupgrade),
"tryceratops" => Ok(Plugin::Tryceratops),
_ => Err(anyhow!("Unknown plugin: {string}")),
}
}
}
impl fmt::Debug for Plugin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Plugin::Flake82020 => "flake8-2020",
Plugin::Flake8Annotations => "flake8-annotations",
Plugin::Flake8Bandit => "flake8-bandit",
Plugin::Flake8BlindExcept => "flake8-blind-except",
Plugin::Flake8BooleanTrap => "flake8-boolean-trap",
Plugin::Flake8Bugbear => "flake8-bugbear",
Plugin::Flake8Builtins => "flake8-builtins",
Plugin::Flake8Commas => "flake8-commas",
Plugin::Flake8Comprehensions => "flake8-comprehensions",
Plugin::Flake8Datetimez => "flake8-datetimez",
Plugin::Flake8Debugger => "flake8-debugger",
Plugin::Flake8Docstrings => "flake8-docstrings",
Plugin::Flake8Eradicate => "flake8-eradicate",
Plugin::Flake8ErrMsg => "flake8-errmsg",
Plugin::Flake8Executable => "flake8-executable",
Plugin::Flake8ImplicitStrConcat => "flake8-implicit-str-concat",
Plugin::Flake8ImportConventions => "flake8-import-conventions",
Plugin::Flake8NoPep420 => "flake8-no-pep420",
Plugin::Flake8Pie => "flake8-pie",
Plugin::Flake8Print => "flake8-print",
Plugin::Flake8PytestStyle => "flake8-pytest-style",
Plugin::Flake8Quotes => "flake8-quotes",
Plugin::Flake8Return => "flake8-return",
Plugin::Flake8Simplify => "flake8-simplify",
Plugin::Flake8TidyImports => "flake8-tidy-imports",
Plugin::Flake8TypeChecking => "flake8-type-checking",
Plugin::Flake8UnusedArguments => "flake8-unused-arguments",
Plugin::Flake8UsePathlib => "flake8-use-pathlib",
Plugin::McCabe => "mccabe",
Plugin::PEP8Naming => "pep8-naming",
Plugin::PandasVet => "pandas-vet",
Plugin::Pyupgrade => "pyupgrade",
Plugin::Tryceratops => "tryceratops",
}
)
}
}
impl From<&Plugin> for Linter {
fn from(plugin: &Plugin) -> Self {
match plugin {
Plugin::Flake82020 => Linter::Flake82020,
Plugin::Flake8Annotations => Linter::Flake8Annotations,
Plugin::Flake8Bandit => Linter::Flake8Bandit,
Plugin::Flake8BlindExcept => Linter::Flake8BlindExcept,
Plugin::Flake8BooleanTrap => Linter::Flake8BooleanTrap,
Plugin::Flake8Bugbear => Linter::Flake8Bugbear,
Plugin::Flake8Builtins => Linter::Flake8Builtins,
Plugin::Flake8Commas => Linter::Flake8Commas,
Plugin::Flake8Comprehensions => Linter::Flake8Comprehensions,
Plugin::Flake8Datetimez => Linter::Flake8Datetimez,
Plugin::Flake8Debugger => Linter::Flake8Debugger,
Plugin::Flake8Docstrings => Linter::Pydocstyle,
Plugin::Flake8Eradicate => Linter::Eradicate,
Plugin::Flake8ErrMsg => Linter::Flake8ErrMsg,
Plugin::Flake8Executable => Linter::Flake8Executable,
Plugin::Flake8ImplicitStrConcat => Linter::Flake8ImplicitStrConcat,
Plugin::Flake8ImportConventions => Linter::Flake8ImportConventions,
Plugin::Flake8NoPep420 => Linter::Flake8NoPep420,
Plugin::Flake8Pie => Linter::Flake8Pie,
Plugin::Flake8Print => Linter::Flake8Print,
Plugin::Flake8PytestStyle => Linter::Flake8PytestStyle,
Plugin::Flake8Quotes => Linter::Flake8Quotes,
Plugin::Flake8Return => Linter::Flake8Return,
Plugin::Flake8Simplify => Linter::Flake8Simplify,
Plugin::Flake8TidyImports => Linter::Flake8TidyImports,
Plugin::Flake8TypeChecking => Linter::Flake8TypeChecking,
Plugin::Flake8UnusedArguments => Linter::Flake8UnusedArguments,
Plugin::Flake8UsePathlib => Linter::Flake8UsePathlib,
Plugin::McCabe => Linter::McCabe,
Plugin::PEP8Naming => Linter::PEP8Naming,
Plugin::PandasVet => Linter::PandasVet,
Plugin::Pyupgrade => Linter::Pyupgrade,
Plugin::Tryceratops => Linter::Tryceratops,
}
}
}
/// Infer the enabled plugins based on user-provided options.
///
/// For example, if the user specified a `mypy-init-return` setting, we should
/// infer that `flake8-annotations` is active.
pub(crate) fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> Vec<Plugin> {
let mut plugins = BTreeSet::new();
for key in flake8.keys() {
match key.as_str() {
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
plugins.insert(Plugin::Flake8Annotations);
}
"suppress-dummy-args" | "suppress_dummy_args" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-untyped-defs" | "allow_untyped_defs" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-untyped-nested" | "allow_untyped_nested" => {
plugins.insert(Plugin::Flake8Annotations);
}
"mypy-init-return" | "mypy_init_return" => {
plugins.insert(Plugin::Flake8Annotations);
}
"dispatch-decorators" | "dispatch_decorators" => {
plugins.insert(Plugin::Flake8Annotations);
}
"overload-decorators" | "overload_decorators" => {
plugins.insert(Plugin::Flake8Annotations);
}
"allow-star-arg-any" | "allow_star_arg_any" => {
plugins.insert(Plugin::Flake8Annotations);
}
// flake8-bugbear
"extend-immutable-calls" | "extend_immutable_calls" => {
plugins.insert(Plugin::Flake8Bugbear);
}
// flake8-builtins
"builtins-ignorelist" | "builtins_ignorelist" => {
plugins.insert(Plugin::Flake8Builtins);
}
// flake8-docstrings
"docstring-convention" | "docstring_convention" => {
plugins.insert(Plugin::Flake8Docstrings);
}
// flake8-eradicate
"eradicate-aggressive" | "eradicate_aggressive" => {
plugins.insert(Plugin::Flake8Eradicate);
}
"eradicate-whitelist" | "eradicate_whitelist" => {
plugins.insert(Plugin::Flake8Eradicate);
}
"eradicate-whitelist-extend" | "eradicate_whitelist_extend" => {
plugins.insert(Plugin::Flake8Eradicate);
}
// flake8-pytest-style
"pytest-fixture-no-parentheses" | "pytest_fixture_no_parentheses " => {
plugins.insert(Plugin::Flake8PytestStyle);
}
"pytest-parametrize-names-type" | "pytest_parametrize_names_type" => {
plugins.insert(Plugin::Flake8PytestStyle);
}
"pytest-parametrize-values-type" | "pytest_parametrize_values_type" => {
plugins.insert(Plugin::Flake8PytestStyle);
}
"pytest-parametrize-values-row-type" | "pytest_parametrize_values_row_type" => {
plugins.insert(Plugin::Flake8PytestStyle);
}
"pytest-raises-require-match-for" | "pytest_raises_require_match_for" => {
plugins.insert(Plugin::Flake8PytestStyle);
}
"pytest-mark-no-parentheses" | "pytest_mark_no_parentheses" => {
plugins.insert(Plugin::Flake8PytestStyle);
}
// flake8-quotes
"quotes" | "inline-quotes" | "inline_quotes" => {
plugins.insert(Plugin::Flake8Quotes);
}
"multiline-quotes" | "multiline_quotes" => {
plugins.insert(Plugin::Flake8Quotes);
}
"docstring-quotes" | "docstring_quotes" => {
plugins.insert(Plugin::Flake8Quotes);
}
"avoid-escape" | "avoid_escape" => {
plugins.insert(Plugin::Flake8Quotes);
}
// flake8-tidy-imports
"ban-relative-imports" | "ban_relative_imports" => {
plugins.insert(Plugin::Flake8TidyImports);
}
"banned-modules" | "banned_modules" => {
plugins.insert(Plugin::Flake8TidyImports);
}
// mccabe
"max-complexity" | "max_complexity" => {
plugins.insert(Plugin::McCabe);
}
// pep8-naming
"ignore-names" | "ignore_names" => {
plugins.insert(Plugin::PEP8Naming);
}
"classmethod-decorators" | "classmethod_decorators" => {
plugins.insert(Plugin::PEP8Naming);
}
"staticmethod-decorators" | "staticmethod_decorators" => {
plugins.insert(Plugin::PEP8Naming);
}
"max-string-length" | "max_string_length" => {
plugins.insert(Plugin::Flake8ErrMsg);
}
_ => {}
}
}
Vec::from_iter(plugins)
}
/// Infer the enabled plugins based on the referenced prefixes.
///
/// For example, if the user ignores `ANN101`, we should infer that
/// `flake8-annotations` is active.
pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec<Plugin> {
// Ignore cases in which we've knowingly changed rule prefixes.
[
Plugin::Flake82020,
Plugin::Flake8Annotations,
Plugin::Flake8Bandit,
// Plugin::Flake8BlindExcept,
Plugin::Flake8BooleanTrap,
Plugin::Flake8Bugbear,
Plugin::Flake8Builtins,
// Plugin::Flake8Commas,
Plugin::Flake8Comprehensions,
Plugin::Flake8Datetimez,
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
// Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8Executable,
Plugin::Flake8ImplicitStrConcat,
// Plugin::Flake8ImportConventions,
Plugin::Flake8NoPep420,
Plugin::Flake8Pie,
Plugin::Flake8Print,
Plugin::Flake8PytestStyle,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
// Plugin::Flake8TidyImports,
// Plugin::Flake8TypeChecking,
Plugin::Flake8UnusedArguments,
// Plugin::Flake8UsePathlib,
Plugin::McCabe,
Plugin::PEP8Naming,
Plugin::PandasVet,
Plugin::Tryceratops,
]
.into_iter()
.filter(|plugin| {
for selector in selectors {
if selector
.rules(&PreviewOptions::default())
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
{
return true;
}
}
false
})
.collect()
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{infer_plugins_from_options, Plugin};
#[test]
fn it_infers_plugins() {
let actual = infer_plugins_from_options(&HashMap::from([(
"inline-quotes".to_string(),
Some("single".to_string()),
)]));
let expected = vec![Plugin::Flake8Quotes];
assert_eq!(actual, expected);
let actual = infer_plugins_from_options(&HashMap::from([(
"staticmethod-decorators".to_string(),
Some("[]".to_string()),
)]));
let expected = vec![Plugin::PEP8Naming];
assert_eq!(actual, expected);
}
}

View File

@@ -0,0 +1,26 @@
use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use super::black::Black;
use super::isort::Isort;
use super::pep621::Project;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Tools {
pub(crate) black: Option<Black>,
pub(crate) isort: Option<Isort>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Pyproject {
pub(crate) tool: Option<Tools>,
pub(crate) project: Option<Project>,
}
pub(crate) fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
let contents = std::fs::read_to_string(path)?;
let pyproject = toml::from_str::<Pyproject>(&contents)?;
Ok(pyproject)
}

View File

@@ -1,2 +0,0 @@
[tool.ruff]
select = []

View File

@@ -1,2 +0,0 @@
[tool.ruff]
include = ["a.py", "subdirectory/c.py"]

View File

@@ -1,413 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "4f8ce941-1492-4d4e-8ab5-70d733fe891a",
"metadata": {},
"outputs": [],
"source": [
"%config ZMQInteractiveShell.ast_node_interactivity=\"last_expr_or_assign\""
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "721ec705-0c65-4bfb-9809-7ed8bc534186",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Assignment statement without a semicolon\n",
"x = 1"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "de50e495-17e5-41cc-94bd-565757555d7e",
"metadata": {},
"outputs": [],
"source": [
"# Assignment statement with a semicolon\n",
"x = 1;\n",
"x = 1;"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "39e31201-23da-44eb-8684-41bba3663991",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"2"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Augmented assignment without a semicolon\n",
"x += 1"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "6b73d3dd-c73a-4697-9e97-e109a6c1fbab",
"metadata": {},
"outputs": [],
"source": [
"# Augmented assignment without a semicolon\n",
"x += 1;\n",
"x += 1; # comment\n",
"# comment"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "2a3e5b86-aa5b-46ba-b9c6-0386d876f58c",
"metadata": {},
"outputs": [],
"source": [
"# Multiple assignment without a semicolon\n",
"x = y = 1"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "07f89e51-9357-4cfb-8fc5-76fb75e35949",
"metadata": {},
"outputs": [],
"source": [
"# Multiple assignment with a semicolon\n",
"x = y = 1;\n",
"x = y = 1;"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "c22b539d-473e-48f8-a236-625e58c47a00",
"metadata": {},
"outputs": [],
"source": [
"# Tuple unpacking without a semicolon\n",
"x, y = 1, 2"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "12c87940-a0d5-403b-a81c-7507eb06dc7e",
"metadata": {},
"outputs": [],
"source": [
"# Tuple unpacking with a semicolon (irrelevant)\n",
"x, y = 1, 2;\n",
"x, y = 1, 2; # comment\n",
"# comment"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "5a768c76-6bc4-470c-b37e-8cc14bc6caf4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Annotated assignment statement without a semicolon\n",
"x: int = 1"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "21bfda82-1a9a-4ba1-9078-74ac480804b5",
"metadata": {},
"outputs": [],
"source": [
"# Annotated assignment statement without a semicolon\n",
"x: int = 1;\n",
"x: int = 1; # comment\n",
"# comment"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "09929999-ff29-4d10-ad2b-e665af15812d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Assignment expression without a semicolon\n",
"(x := 1)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "32a83217-1bad-4f61-855e-ffcdb119c763",
"metadata": {},
"outputs": [],
"source": [
"# Assignment expression with a semicolon\n",
"(x := 1);\n",
"(x := 1); # comment\n",
"# comment"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "61b81865-277e-4964-b03e-eb78f1f318eb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"x = 1\n",
"# Expression without a semicolon\n",
"x"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "974c29be-67e1-4000-95fa-6ca118a63bad",
"metadata": {},
"outputs": [],
"source": [
"x = 1\n",
"# Expression with a semicolon\n",
"x;"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "cfeb1757-46d6-4f13-969f-a283b6d0304f",
"metadata": {},
"outputs": [],
"source": [
"class Point:\n",
" def __init__(self, x, y):\n",
" self.x = x\n",
" self.y = y\n",
"\n",
"\n",
"p = Point(0, 0);"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "2ee7f1a5-ccfe-4004-bfa4-ef834a58da97",
"metadata": {},
"outputs": [],
"source": [
"# Assignment statement where the left is an attribute access doesn't\n",
"# print the value.\n",
"p.x = 1;"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "3e49370a-048b-474d-aa0a-3d1d4a73ad37",
"metadata": {},
"outputs": [],
"source": [
"data = {}\n",
"\n",
"# Neither does the subscript node\n",
"data[\"foo\"] = 1;"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "d594bdd3-eaa9-41ef-8cda-cf01bc273b2d",
"metadata": {},
"outputs": [],
"source": [
"if (x := 1):\n",
" # It should be the top level statement\n",
" x"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "e532f0cf-80c7-42b7-8226-6002fcf74fb6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Parentheses with comments\n",
"(\n",
" x := 1 # comment\n",
") # comment"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "473c5d62-871b-46ed-8a34-27095243f462",
"metadata": {},
"outputs": [],
"source": [
"# Parentheses with comments\n",
"(\n",
" x := 1 # comment\n",
"); # comment"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "8c3c2361-f49f-45fe-bbe3-7e27410a8a86",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Hello world!'"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\"\"\"Hello world!\"\"\""
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "23dbe9b5-3f68-4890-ab2d-ab0dbfd0712a",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"Hello world!\"\"\"; # comment\n",
"# comment"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "3ce33108-d95d-4c70-83d1-0d4fd36a2951",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'x = 1'"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"x = 1\n",
"f\"x = {x}\""
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "654a4a67-de43-4684-824a-9451c67db48f",
"metadata": {},
"outputs": [],
"source": [
"x = 1\n",
"f\"x = {x}\";\n",
"f\"x = {x}\"; # comment\n",
"# comment"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff-playground)",
"language": "python",
"name": "ruff-playground"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
use crate::ExitStatus;
use anyhow::Result;
use ruff_linter::logging::LogLevel;
use ruff_server::Server;
use tracing::{level_filters::LevelFilter, metadata::Level, subscriber::Interest, Metadata};
use tracing_subscriber::{
layer::{Context, Filter, SubscriberExt},
Layer, Registry,
};
use tracing_tree::time::Uptime;
pub(crate) fn run_server(preview: bool, log_level: LogLevel) -> Result<ExitStatus> {
if !preview {
tracing::error!("--preview needs to be provided as a command line argument while the server is still unstable.\nFor example: `ruff server --preview`");
return Ok(ExitStatus::Error);
}
let trace_level = if log_level == LogLevel::Verbose {
Level::TRACE
} else {
Level::DEBUG
};
let subscriber = Registry::default().with(
tracing_tree::HierarchicalLayer::default()
.with_indent_lines(true)
.with_indent_amount(2)
.with_bracketed_fields(true)
.with_targets(true)
.with_writer(|| Box::new(std::io::stderr()))
.with_timer(Uptime::default())
.with_filter(LoggingFilter { trace_level }),
);
tracing::subscriber::set_global_default(subscriber)?;
let server = Server::new()?;
server.run().map(|()| ExitStatus::Success)
}
struct LoggingFilter {
trace_level: Level,
}
impl LoggingFilter {
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
let filter = if meta.target().starts_with("ruff") {
self.trace_level
} else {
Level::INFO
};
meta.level() <= &filter
}
}
impl<S> Filter<S> for LoggingFilter {
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
self.is_enabled(meta)
}
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
if self.is_enabled(meta) {
Interest::always()
} else {
Interest::never()
}
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(LevelFilter::from_level(self.trace_level))
}
}

View File

@@ -1,5 +0,0 @@
---
source: crates/ruff/src/version.rs
expression: version
---
0.0.0

View File

@@ -1,17 +0,0 @@
use std::io;
use std::io::{Read, Write};
/// Read a string from `stdin`.
pub(crate) fn read_from_stdin() -> Result<String, io::Error> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
/// Read bytes from `stdin` and write them to `stdout`.
pub(crate) fn parrot_stdin() -> Result<(), io::Error> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
io::stdout().write_all(buffer.as_bytes())?;
Ok(())
}

View File

@@ -1,150 +0,0 @@
//! A test suite that ensures deprecated command line options have appropriate warnings / behaviors
use ruff_linter::settings::types::SerializationFormat;
use std::process::Command;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "ruff";
const STDIN: &str = "l = 1";
fn ruff_check(show_source: Option<bool>, output_format: Option<String>) -> Command {
let mut cmd = Command::new(get_cargo_bin(BIN_NAME));
let output_format = output_format.unwrap_or(format!("{}", SerializationFormat::default(false)));
cmd.arg("check")
.arg("--output-format")
.arg(output_format)
.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");
}
Some(false) => {
cmd.arg("--no-show-source");
}
None => {}
}
cmd.arg("-");
cmd
}
#[test]
fn ensure_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(true), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_no_show_source_is_deprecated() {
assert_cmd_snapshot!(ruff_check(Some(false), None).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_output_format_is_deprecated() {
assert_cmd_snapshot!(ruff_check(None, Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_full_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("full".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
|
1 | l = 1
| ^ E741
|
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=full`.
"###);
}
#[test]
fn ensure_output_format_uses_concise_over_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("concise".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_show_source() {
assert_cmd_snapshot!(ruff_check(Some(true), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}
#[test]
fn ensure_deprecated_output_format_overrides_no_show_source() {
assert_cmd_snapshot!(ruff_check(Some(false), Some("text".into())).pass_stdin(STDIN), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:1: E741 Ambiguous variable name: `l`
Found 1 error.
----- stderr -----
warning: The `--no-show-source` argument is deprecated and has been ignored in favor of `--output-format=text`.
warning: `--output-format=text` is deprecated. Use `--output-format=full` or `--output-format=concise` instead. `text` will be treated as `concise`.
"###);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,103 +0,0 @@
#![cfg(not(target_family = "wasm"))]
use std::path::Path;
use std::process::Command;
use std::str;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
const BIN_NAME: &str = "ruff";
#[cfg(not(target_os = "windows"))]
const TEST_FILTERS: &[(&str, &str)] = &[(".*/resources/test/fixtures/", "[BASEPATH]/")];
#[cfg(target_os = "windows")]
const TEST_FILTERS: &[(&str, &str)] = &[
(r".*\\resources\\test\\fixtures\\", "[BASEPATH]\\"),
(r"\\", "/"),
];
#[test]
fn check_project_include_defaults() {
// Defaults to checking the current working directory
//
// The test directory includes:
// - A pyproject.toml which specifies an include
// - A nested pyproject.toml which has a Ruff section
//
// The nested project should all be checked instead of respecting the parent includes
insta::with_settings!({
filters => TEST_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
success: true
exit_code: 0
----- stdout -----
[BASEPATH]/include-test/a.py
[BASEPATH]/include-test/nested-project/e.py
[BASEPATH]/include-test/nested-project/pyproject.toml
[BASEPATH]/include-test/subdirectory/c.py
----- stderr -----
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `nested-project/pyproject.toml`:
- 'select' -> 'lint.select'
"###);
});
}
#[test]
fn check_project_respects_direct_paths() {
// Given a direct path not included in the project `includes`, it should be checked
insta::with_settings!({
filters => TEST_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-files", "b.py"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
success: true
exit_code: 0
----- stdout -----
[BASEPATH]/include-test/b.py
----- stderr -----
"###);
});
}
#[test]
fn check_project_respects_subdirectory_includes() {
// Given a direct path to a subdirectory, the include should be respected
insta::with_settings!({
filters => TEST_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-files", "subdirectory"]).current_dir(Path::new("./resources/test/fixtures/include-test")), @r###"
success: true
exit_code: 0
----- stdout -----
[BASEPATH]/include-test/subdirectory/c.py
----- stderr -----
"###);
});
}
#[test]
fn check_project_from_project_subdirectory_respects_includes() {
// Run from a project subdirectory, the include specified in the parent directory should be respected
insta::with_settings!({
filters => TEST_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-files"]).current_dir(Path::new("./resources/test/fixtures/include-test/subdirectory")), @r###"
success: true
exit_code: 0
----- stdout -----
[BASEPATH]/include-test/subdirectory/c.py
----- stderr -----
"###);
});
}

View File

@@ -1,33 +0,0 @@
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use std::path::Path;
use std::process::Command;
const BIN_NAME: &str = "ruff";
#[test]
fn display_default_settings() {
// Navigate from the crate directory to the workspace root.
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let base_path = base_path.to_string_lossy();
// Escape the backslashes for the regex.
let base_path = regex::escape(&base_path);
#[cfg(not(target_os = "windows"))]
let test_filters = &[(base_path.as_ref(), "[BASEPATH]")];
#[cfg(target_os = "windows")]
let test_filters = &[
(base_path.as_ref(), "[BASEPATH]"),
(r#"\\+(\w\w|\s|\.|")"#, "/$1"),
];
insta::with_settings!({ filters => test_filters.to_vec() }, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
});
}

View File

@@ -1,383 +0,0 @@
---
source: crates/ruff/tests/show_settings.rs
info:
program: ruff
args:
- check
- "--show-settings"
- unformatted.py
---
success: true
exit_code: 0
----- stdout -----
Resolved settings for: "[BASEPATH]/crates/ruff/resources/test/fixtures/unformatted.py"
Settings path: "[BASEPATH]/pyproject.toml"
# General Settings
cache_dir = "[BASEPATH]/.ruff_cache"
fix = false
fix_only = false
output_format = concise
show_fixes = false
unsafe_fixes = hint
# File Resolver Settings
file_resolver.exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"dist",
"node_modules",
"site-packages",
"venv",
]
file_resolver.extend_exclude = [
"crates/ruff_linter/resources/",
"crates/ruff_python_formatter/resources/",
]
file_resolver.force_exclude = false
file_resolver.include = [
"*.py",
"*.pyi",
"**/pyproject.toml",
]
file_resolver.extend_include = []
file_resolver.respect_gitignore = true
file_resolver.project_root = "[BASEPATH]"
# Linter Settings
linter.exclude = []
linter.project_root = "[BASEPATH]"
linter.rules.enabled = [
MultipleImportsOnOneLine,
ModuleImportNotAtTopOfFile,
MultipleStatementsOnOneLineColon,
MultipleStatementsOnOneLineSemicolon,
UselessSemicolon,
NoneComparison,
TrueFalseComparison,
NotInTest,
NotIsTest,
TypeComparison,
BareExcept,
LambdaAssignment,
AmbiguousVariableName,
AmbiguousClassName,
AmbiguousFunctionName,
IOError,
SyntaxError,
UnusedImport,
ImportShadowedByLoopVar,
UndefinedLocalWithImportStar,
LateFutureImport,
UndefinedLocalWithImportStarUsage,
UndefinedLocalWithNestedImportStarUsage,
FutureFeatureNotDefined,
PercentFormatInvalidFormat,
PercentFormatExpectedMapping,
PercentFormatExpectedSequence,
PercentFormatExtraNamedArguments,
PercentFormatMissingArgument,
PercentFormatMixedPositionalAndNamed,
PercentFormatPositionalCountMismatch,
PercentFormatStarRequiresSequence,
PercentFormatUnsupportedFormatCharacter,
StringDotFormatInvalidFormat,
StringDotFormatExtraNamedArguments,
StringDotFormatExtraPositionalArguments,
StringDotFormatMissingArguments,
StringDotFormatMixingAutomatic,
FStringMissingPlaceholders,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable,
ExpressionsInStarAssignment,
MultipleStarredExpressions,
AssertTuple,
IsLiteral,
InvalidPrintSyntax,
IfTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
YieldOutsideFunction,
ReturnOutsideFunction,
DefaultExceptNotLast,
ForwardAnnotationSyntaxError,
RedefinedWhileUnused,
UndefinedName,
UndefinedExport,
UndefinedLocal,
UnusedVariable,
UnusedAnnotation,
RaiseNotImplemented,
]
linter.rules.should_fix = [
MultipleImportsOnOneLine,
ModuleImportNotAtTopOfFile,
MultipleStatementsOnOneLineColon,
MultipleStatementsOnOneLineSemicolon,
UselessSemicolon,
NoneComparison,
TrueFalseComparison,
NotInTest,
NotIsTest,
TypeComparison,
BareExcept,
LambdaAssignment,
AmbiguousVariableName,
AmbiguousClassName,
AmbiguousFunctionName,
IOError,
SyntaxError,
UnusedImport,
ImportShadowedByLoopVar,
UndefinedLocalWithImportStar,
LateFutureImport,
UndefinedLocalWithImportStarUsage,
UndefinedLocalWithNestedImportStarUsage,
FutureFeatureNotDefined,
PercentFormatInvalidFormat,
PercentFormatExpectedMapping,
PercentFormatExpectedSequence,
PercentFormatExtraNamedArguments,
PercentFormatMissingArgument,
PercentFormatMixedPositionalAndNamed,
PercentFormatPositionalCountMismatch,
PercentFormatStarRequiresSequence,
PercentFormatUnsupportedFormatCharacter,
StringDotFormatInvalidFormat,
StringDotFormatExtraNamedArguments,
StringDotFormatExtraPositionalArguments,
StringDotFormatMissingArguments,
StringDotFormatMixingAutomatic,
FStringMissingPlaceholders,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable,
ExpressionsInStarAssignment,
MultipleStarredExpressions,
AssertTuple,
IsLiteral,
InvalidPrintSyntax,
IfTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
YieldOutsideFunction,
ReturnOutsideFunction,
DefaultExceptNotLast,
ForwardAnnotationSyntaxError,
RedefinedWhileUnused,
UndefinedName,
UndefinedExport,
UndefinedLocal,
UnusedVariable,
UnusedAnnotation,
RaiseNotImplemented,
]
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.target_version = Py37
linter.preview = disabled
linter.explicit_preview_rules = false
linter.extension.mapping = {}
linter.allowed_confusables = []
linter.builtins = []
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
linter.external = []
linter.ignore_init_module_imports = true
linter.logger_objects = []
linter.namespace_packages = []
linter.src = [
"[BASEPATH]",
]
linter.tab_size = 4
linter.line_length = 88
linter.task_tags = [
TODO,
FIXME,
XXX,
]
linter.typing_modules = []
# Linter Plugins
linter.flake8_annotations.mypy_init_return = false
linter.flake8_annotations.suppress_dummy_args = false
linter.flake8_annotations.suppress_none_returning = false
linter.flake8_annotations.allow_star_arg_any = false
linter.flake8_annotations.ignore_fully_untyped = false
linter.flake8_bandit.hardcoded_tmp_directory = [
/tmp,
/var/tmp,
/dev/shm,
]
linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0
linter.flake8_gettext.functions_names = [
_,
gettext,
ngettext,
]
linter.flake8_implicit_str_concat.allow_multiline = true
linter.flake8_import_conventions.aliases = {
altair = alt,
holoviews = hv,
matplotlib = mpl,
matplotlib.pyplot = plt,
networkx = nx,
numpy = np,
pandas = pd,
panel = pn,
plotly.express = px,
polars = pl,
pyarrow = pa,
seaborn = sns,
tensorflow = tf,
tkinter = tk,
}
linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = true
linter.flake8_pytest_style.parametrize_names_type = tuple
linter.flake8_pytest_style.parametrize_values_type = list
linter.flake8_pytest_style.parametrize_values_row_type = tuple
linter.flake8_pytest_style.raises_require_match_for = [
BaseException,
Exception,
ValueError,
OSError,
IOError,
EnvironmentError,
socket.error,
]
linter.flake8_pytest_style.raises_extend_require_match_for = []
linter.flake8_pytest_style.mark_parentheses = true
linter.flake8_quotes.inline_quotes = double
linter.flake8_quotes.multiline_quotes = double
linter.flake8_quotes.docstring_quotes = double
linter.flake8_quotes.avoid_escape = true
linter.flake8_self.ignore_names = [
_make,
_asdict,
_replace,
_fields,
_field_defaults,
_name_,
_value_,
]
linter.flake8_tidy_imports.ban_relative_imports = "parents"
linter.flake8_tidy_imports.banned_api = {}
linter.flake8_tidy_imports.banned_module_level_imports = []
linter.flake8_type_checking.strict = false
linter.flake8_type_checking.exempt_modules = [
typing,
typing_extensions,
]
linter.flake8_type_checking.runtime_required_base_classes = []
linter.flake8_type_checking.runtime_required_decorators = []
linter.flake8_type_checking.quote_annotations = false
linter.flake8_unused_arguments.ignore_variadic_names = false
linter.isort.required_imports = []
linter.isort.combine_as_imports = false
linter.isort.force_single_line = false
linter.isort.force_sort_within_sections = false
linter.isort.detect_same_package = true
linter.isort.case_sensitive = false
linter.isort.force_wrap_aliases = false
linter.isort.force_to_top = []
linter.isort.known_modules = {}
linter.isort.order_by_type = true
linter.isort.relative_imports_order = furthest_to_closest
linter.isort.single_line_exclusions = []
linter.isort.split_on_trailing_comma = true
linter.isort.classes = []
linter.isort.constants = []
linter.isort.variables = []
linter.isort.no_lines_before = []
linter.isort.lines_after_imports = -1
linter.isort.lines_between_types = 0
linter.isort.forced_separate = []
linter.isort.section_order = [
known { type = future },
known { type = standard_library },
known { type = third_party },
known { type = first_party },
known { type = local_folder },
]
linter.isort.default_section = known { type = third_party }
linter.isort.no_sections = false
linter.isort.from_first = false
linter.isort.length_sort = false
linter.isort.length_sort_straight = false
linter.mccabe.max_complexity = 10
linter.pep8_naming.ignore_names = [
setUp,
tearDown,
setUpClass,
tearDownClass,
setUpModule,
tearDownModule,
asyncSetUp,
asyncTearDown,
setUpTestData,
failureException,
longMessage,
maxDiff,
]
linter.pep8_naming.classmethod_decorators = []
linter.pep8_naming.staticmethod_decorators = []
linter.pycodestyle.max_line_length = 88
linter.pycodestyle.max_doc_length = none
linter.pycodestyle.ignore_overlong_task_comments = false
linter.pyflakes.extend_generics = []
linter.pylint.allow_magic_value_types = [
str,
bytes,
]
linter.pylint.allow_dunder_method_names = []
linter.pylint.max_args = 5
linter.pylint.max_positional_args = 5
linter.pylint.max_returns = 6
linter.pylint.max_bool_expr = 5
linter.pylint.max_branches = 12
linter.pylint.max_statements = 50
linter.pylint.max_public_methods = 20
linter.pylint.max_locals = 15
linter.pyupgrade.keep_runtime_typing = false
# Formatter Settings
formatter.exclude = []
formatter.target_version = Py37
formatter.preview = disabled
formatter.line_width = 88
formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
----- stderr -----

View File

@@ -1,105 +0,0 @@
//! Tests for the --version command
use std::fs;
use std::process::Command;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const VERSION_FILTER: [(&str, &str); 1] = [(
r"\d+\.\d+\.\d+(\+\d+)?( \(\w{9} \d\d\d\d-\d\d-\d\d\))?",
"[VERSION]",
)];
#[test]
fn version_basics() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}
/// `--config` is a global option,
/// so it's allowed to pass --config to subcommands such as `version`
/// -- the flag is simply ignored
#[test]
fn config_option_allowed_but_ignored() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.arg("--config")
.arg(&ruff_dot_toml)
.args(["--config", "lint.isort.extra-standard-library = ['foo', 'bar']"]), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
Ok(())
}
#[test]
fn config_option_ignored_but_validated() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("version")
.args(["--config", "foo = bar"]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'foo = bar' for '--config <CONFIG_OPTION>'
tip: A `--config` flag must either be a path to a `.toml` configuration file
or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
option
The supplied argument is not valid TOML:
TOML parse error at line 1, column 7
|
1 | foo = bar
| ^
invalid string
expected `"`, `'`
For more information, try '--help'.
"###
);
});
}
/// `--isolated` is also a global option,
#[test]
fn isolated_option_allowed() {
insta::with_settings!({filters => VERSION_FILTER.to_vec()}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).arg("version").arg("--isolated"), @r###"
success: true
exit_code: 0
----- stdout -----
ruff [VERSION]
----- stderr -----
"###
);
});
}

View File

@@ -13,7 +13,6 @@ license = { workspace = true }
[lib] [lib]
bench = false bench = false
doctest = false
[[bench]] [[bench]]
name = "linter" name = "linter"
@@ -32,29 +31,26 @@ name = "formatter"
harness = false harness = false
[dependencies] [dependencies]
once_cell = { workspace = true } once_cell.workspace = true
serde = { workspace = true } serde.workspace = true
serde_json = { workspace = true } serde_json.workspace = true
url = { workspace = true } url = "2.3.1"
ureq = { workspace = true } ureq = "2.8.0"
criterion = { workspace = true, default-features = false } criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { workspace = true, default-features = false, optional = true} codspeed-criterion-compat = { version="2.3.0", default-features = false, optional = true}
[dev-dependencies] [dev-dependencies]
ruff_linter = { path = "../ruff_linter" } ruff_linter.path = "../ruff_linter"
ruff_python_ast = { path = "../ruff_python_ast" } ruff_python_ast.path = "../ruff_python_ast"
ruff_python_formatter = { path = "../ruff_python_formatter" } ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_index = { path = "../ruff_python_index" } ruff_python_index = { path = "../ruff_python_index" }
ruff_python_parser = { path = "../ruff_python_parser" } ruff_python_parser = { path = "../ruff_python_parser" }
[lints]
workspace = true
[features] [features]
codspeed = ["codspeed-criterion-compat"] codspeed = ["codspeed-criterion-compat"]
[target.'cfg(target_os = "windows")'.dev-dependencies] [target.'cfg(target_os = "windows")'.dev-dependencies]
mimalloc = { workspace = true } mimalloc = "0.1.39"
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies] [target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies]
tikv-jemallocator = { workspace = true } tikv-jemallocator = "0.5.0"

View File

@@ -1,16 +1,5 @@
# Ruff Benchmarks # Ruff Benchmarks
The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files: The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files.
```shell
# Run once on the "baseline".
cargo bench -p ruff_benchmark -- --save-baseline=main
# Compare against the "baseline".
cargo bench -p ruff_benchmark -- --baseline=main
# Run the lexer benchmarks.
cargo bench -p ruff_benchmark lexer -- --baseline=main
```
See [CONTRIBUTING.md](../../CONTRIBUTING.md) on how to use these benchmarks. See [CONTRIBUTING.md](../../CONTRIBUTING.md) on how to use these benchmarks.

View File

@@ -4,10 +4,10 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
}; };
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError}; use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions}; use ruff_python_formatter::{format_module_ast, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder; use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex; use ruff_python_parser::lexer::lex;
use ruff_python_parser::{allocate_tokens_vec, parse_tokens, Mode}; use ruff_python_parser::{parse_tokens, Mode};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[global_allocator] #[global_allocator]
@@ -52,7 +52,7 @@ fn benchmark_formatter(criterion: &mut Criterion) {
BenchmarkId::from_parameter(case.name()), BenchmarkId::from_parameter(case.name()),
&case, &case,
|b, case| { |b, case| {
let mut tokens = allocate_tokens_vec(case.code()); let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default(); let mut comment_ranges = CommentRangesBuilder::default();
for result in lex(case.code(), Mode::Module) { for result in lex(case.code(), Mode::Module) {
@@ -65,12 +65,11 @@ fn benchmark_formatter(criterion: &mut Criterion) {
let comment_ranges = comment_ranges.finish(); let comment_ranges = comment_ranges.finish();
// Parse the AST. // Parse the AST.
let module = parse_tokens(tokens, case.code(), Mode::Module) let module = parse_tokens(tokens, case.code(), Mode::Module, "<filename>")
.expect("Input to be a valid python program"); .expect("Input to be a valid python program");
b.iter(|| { b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name())) let options = PyFormatOptions::from_extension(Path::new(case.name()));
.with_preview(PreviewMode::Enabled);
let formatted = let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options) format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed"); .expect("Formatting to succeed");

View File

@@ -2,15 +2,12 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput,
}; };
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError}; use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_linter::linter::{lint_only, ParseSource}; use ruff_linter::linter::lint_only;
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable; use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::PreviewMode;
use ruff_linter::settings::{flags, LinterSettings}; use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind; use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector}; use ruff_linter::{registry::Rule, RuleSelector};
use ruff_python_ast::PySourceType; use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, parse_program_tokens, Mode};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[global_allocator] #[global_allocator]
@@ -54,12 +51,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
BenchmarkId::from_parameter(case.name()), BenchmarkId::from_parameter(case.name()),
&case, &case,
|b, case| { |b, case| {
// Tokenize the source. let kind = SourceKind::Python(case.code().to_string());
let tokens: Vec<_> = lexer::lex(case.code(), Mode::Module).collect();
// Parse the source.
let ast = parse_program_tokens(tokens.clone(), case.code(), false).unwrap();
b.iter(|| { b.iter(|| {
let path = case.path(); let path = case.path();
let result = lint_only( let result = lint_only(
@@ -67,12 +59,8 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
None, None,
settings, settings,
flags::Noqa::Enabled, flags::Noqa::Enabled,
&SourceKind::Python(case.code().to_string()), &kind,
PySourceType::from(path.as_path()), PySourceType::from(path.as_path()),
ParseSource::Precomputed {
tokens: &tokens,
ast: &ast,
},
); );
// Assert that file contains no parse errors // Assert that file contains no parse errors
@@ -90,21 +78,12 @@ fn benchmark_default_rules(criterion: &mut Criterion) {
benchmark_linter(group, &LinterSettings::default()); benchmark_linter(group, &LinterSettings::default());
} }
/// Disables IO based rules because they are a source of flakiness fn benchmark_all_rules(criterion: &mut Criterion) {
fn disable_io_rules(rules: &mut RuleTable) { let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
// Disable IO based rules because it is a source of flakiness
rules.disable(Rule::ShebangMissingExecutableFile); rules.disable(Rule::ShebangMissingExecutableFile);
rules.disable(Rule::ShebangNotExecutable); rules.disable(Rule::ShebangNotExecutable);
}
fn benchmark_all_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All
.rules(&PreviewOptions {
mode: PreviewMode::Disabled,
require_explicit: false,
})
.collect();
disable_io_rules(&mut rules);
let settings = LinterSettings { let settings = LinterSettings {
rules, rules,
@@ -115,22 +94,6 @@ fn benchmark_all_rules(criterion: &mut Criterion) {
benchmark_linter(group, &settings); benchmark_linter(group, &settings);
} }
fn benchmark_preview_rules(criterion: &mut Criterion) {
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
disable_io_rules(&mut rules);
let settings = LinterSettings {
rules,
preview: PreviewMode::Enabled,
..LinterSettings::default()
};
let group = criterion.benchmark_group("linter/all-with-preview-rules");
benchmark_linter(group, &settings);
}
criterion_group!(default_rules, benchmark_default_rules); criterion_group!(default_rules, benchmark_default_rules);
criterion_group!(all_rules, benchmark_all_rules); criterion_group!(all_rules, benchmark_all_rules);
criterion_group!(preview_rules, benchmark_preview_rules); criterion_main!(default_rules, all_rules);
criterion_main!(default_rules, all_rules, preview_rules);

View File

@@ -60,7 +60,7 @@ fn benchmark_parser(criterion: &mut Criterion<WallTime>) {
&case, &case,
|b, case| { |b, case| {
b.iter(|| { b.iter(|| {
let parsed = parse_suite(case.code()).unwrap(); let parsed = parse_suite(case.code(), case.name()).unwrap();
let mut visitor = CountVisitor { count: 0 }; let mut visitor = CountVisitor { count: 0 };
visitor.visit_body(&parsed); visitor.visit_body(&parsed);

View File

@@ -16,10 +16,7 @@ glob = { workspace = true }
globset = { workspace = true } globset = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
filetime = { workspace = true } filetime = { workspace = true }
seahash = { workspace = true } seahash = "4.1.0"
[dev-dependencies] [dev-dependencies]
ruff_macros = { path = "../ruff_macros" } ruff_macros = { path = "../ruff_macros" }
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ruff" name = "ruff_cli"
version = "0.3.2" version = "0.1.3"
publish = false publish = false
authors = { workspace = true } authors = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
@@ -10,70 +10,69 @@ documentation = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
license = { workspace = true } license = { workspace = true }
readme = "../../README.md" readme = "../../README.md"
default-run = "ruff"
[[bin]]
name = "ruff"
[dependencies] [dependencies]
ruff_linter = { path = "../ruff_linter", features = ["clap"] }
ruff_cache = { path = "../ruff_cache" } ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics" } ruff_diagnostics = { path = "../ruff_diagnostics" }
ruff_linter = { path = "../ruff_linter", features = ["clap"] } ruff_formatter = { path = "../ruff_formatter" }
ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" } ruff_notebook = { path = "../ruff_notebook" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast" } ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" } ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" } ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" } ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_workspace = { path = "../ruff_workspace" } ruff_workspace = { path = "../ruff_workspace" }
ruff_text_size = { path = "../ruff_text_size" }
annotate-snippets = { version = "0.9.1", features = ["color"] }
anyhow = { workspace = true } anyhow = { workspace = true }
argfile = { workspace = true } argfile = { version = "0.1.6" }
bincode = { workspace = true } bincode = { version = "1.3.3" }
bitflags = { workspace = true } bitflags = { workspace = true }
cachedir = { workspace = true } cachedir = { version = "0.3.0" }
chrono = { workspace = true } chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] } clap = { workspace = true, features = ["derive", "env"] }
clap_complete_command = { workspace = true } clap_complete_command = { version = "0.5.1" }
clearscreen = { workspace = true } clearscreen = { version = "2.0.0" }
colored = { workspace = true } colored = { workspace = true }
filetime = { workspace = true } filetime = { workspace = true }
glob = { workspace = true }
ignore = { workspace = true } ignore = { workspace = true }
is-macro = { workspace = true } is-macro = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
log = { workspace = true } log = { workspace = true }
notify = { workspace = true } notify = { version = "6.1.1" }
path-absolutize = { workspace = true, features = ["once_cell_cache"] } path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { workspace = true } rayon = { version = "1.8.0" }
regex = { workspace = true } regex = { workspace = true }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
shellexpand = { workspace = true } shellexpand = { workspace = true }
similar = { workspace = true }
strum = { workspace = true, features = [] } strum = { workspace = true, features = [] }
tempfile = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true, features = ["log"] } tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["registry"]} walkdir = { version = "2.3.2" }
tracing-tree = { workspace = true } wild = { version = "2" }
walkdir = { workspace = true }
wild = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Enable test rules during development assert_cmd = { version = "2.0.8" }
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
assert_cmd = { workspace = true }
# Avoid writing colored snapshots when running tests from the terminal # Avoid writing colored snapshots when running tests from the terminal
colored = { workspace = true, features = ["no-color"]} colored = { workspace = true, features = ["no-color"]}
insta = { workspace = true, features = ["filters", "json"] } insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { workspace = true } insta-cmd = { version = "0.4.0" }
tempfile = { workspace = true } tempfile = "3.8.1"
test-case = { workspace = true } test-case = { workspace = true }
ureq = { version = "2.8.0", features = [] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { workspace = true } mimalloc = "0.1.39"
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies] [target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
tikv-jemallocator = { workspace = true } tikv-jemallocator = "0.5.0"
[lints]
workspace = true

752
crates/ruff_cli/src/args.rs Normal file
View File

@@ -0,0 +1,752 @@
use std::path::PathBuf;
use clap::{command, Parser};
use regex::Regex;
use rustc_hash::FxHashMap;
use ruff_linter::line_width::LineLength;
use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule;
use ruff_linter::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
UnsafeFixes,
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::PycodestyleOptions;
use ruff_workspace::resolver::ConfigurationTransformer;
#[derive(Debug, Parser)]
#[command(
author,
name = "ruff",
about = "Ruff: An extremely fast Python linter.",
after_help = "For help with a specific command, see: `ruff help <command>`."
)]
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
#[clap(flatten)]
pub log_level_args: LogLevelArgs,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Run Ruff on the given files or directories (default).
Check(CheckCommand),
/// Explain a rule (or all rules).
#[clap(alias = "--explain")]
#[command(group = clap::ArgGroup::new("selector").multiple(false).required(true))]
Rule {
/// Rule to explain
#[arg(value_parser=RuleParser, group = "selector", hide_possible_values = true)]
rule: Option<Rule>,
/// Explain all rules
#[arg(long, conflicts_with = "rule", group = "selector")]
all: bool,
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// List or describe the available configuration options.
Config { option: Option<String> },
/// List all supported upstream linters.
Linter {
/// Output format
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
/// Output format (Deprecated: Use `--output-format` instead).
#[arg(long, value_enum, conflicts_with = "output_format", hide = true)]
format: Option<HelpFormat>,
},
/// Clear any caches in the current directory and any subdirectories.
#[clap(alias = "--clean")]
Clean,
/// Generate shell completion.
#[clap(alias = "--generate-shell-completion", hide = true)]
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories.
Format(FormatCommand),
/// Display Ruff's version
Version {
#[arg(long, value_enum, default_value = "text")]
output_format: HelpFormat,
},
}
// The `Parser` derive is for ruff_dev, for ruff_cli `Args` would be sufficient
#[derive(Clone, Debug, clap::Parser)]
#[allow(clippy::struct_excessive_bools)]
pub struct CheckCommand {
/// List of files or directories to check.
pub files: Vec<PathBuf>,
/// Apply fixes to resolve lint violations.
/// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix"))]
fix: bool,
#[clap(long, overrides_with("fix"), hide = true)]
no_fix: bool,
/// Include fixes that may not retain the original intent of the code.
/// Use `--no-unsafe-fixes` to disable.
#[arg(long, overrides_with("no_unsafe_fixes"))]
unsafe_fixes: bool,
#[arg(long, overrides_with("unsafe_fixes"), hide = true)]
no_unsafe_fixes: bool,
/// Show violations with source code.
/// Use `--no-show-source` to disable.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Show an enumeration of all fixed lint violations.
/// Use `--no-show-fixes` to disable.
#[arg(long, overrides_with("no_show_fixes"))]
show_fixes: bool,
#[clap(long, overrides_with("show_fixes"), hide = true)]
no_show_fixes: bool,
/// Avoid writing any fixed files back; instead, output a diff for each changed file to stdout. Implies `--fix-only`.
#[arg(long, conflicts_with = "show_fixes")]
pub diff: bool,
/// Run in watch mode by re-running whenever files change.
#[arg(short, long)]
pub watch: bool,
/// Apply fixes to resolve lint violations, but don't report on leftover violations. Implies `--fix`.
/// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes.
#[arg(long, overrides_with("no_fix_only"))]
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
no_fix_only: bool,
/// Ignore any `# noqa` comments.
#[arg(long)]
ignore_noqa: bool,
/// Output serialization format for violations.
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
pub output_format: Option<SerializationFormat>,
/// Specify file to write the linter output to (default: stdout).
#[arg(short, long)]
pub output_file: Option<PathBuf>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
pub target_version: Option<PythonVersion>,
/// Enable preview mode; checks will include unstable rules and fixes.
/// Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all rules).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub ignore: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of those already specified.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub extend_select: Option<Vec<RuleSelector>>,
/// Like --ignore. (Deprecated: You can just use --ignore instead.)
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide = true
)]
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude.
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Like `--per-file-ignores`, but adds additional ignores on top of those already specified.
#[arg(long, value_delimiter = ',', help_heading = "Rule selection")]
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of those already excluded.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub extend_exclude: Option<Vec<FilePattern>>,
/// List of rule codes to treat as eligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`).
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub unfixable: Option<Vec<RuleSelector>>,
/// Like --fixable, but adds additional rule codes on top of those already specified.
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide_possible_values = true
)]
pub extend_fixable: Option<Vec<RuleSelector>>,
/// Like --unfixable. (Deprecated: You can just use --unfixable instead.)
#[arg(
long,
value_delimiter = ',',
value_name = "RULE_CODE",
value_parser = RuleSelectorParser,
help_heading = "Rule selection",
hide = true
)]
pub extend_unfixable: Option<Vec<RuleSelector>>,
/// Respect file exclusions via `.gitignore` and other standard ignore files.
/// Use `--no-respect-gitignore` to disable.
#[arg(
long,
overrides_with("no_respect_gitignore"),
help_heading = "File selection"
)]
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
/// Use `--no-force-exclude` to disable.
#[arg(
long,
overrides_with("no_force_exclude"),
help_heading = "File selection"
)]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Set the line-length for length-associated rules and automatic formatting.
#[arg(long, help_heading = "Rule configuration", hide = true)]
pub line_length: Option<LineLength>,
/// Regular expression matching the name of dummy variables.
#[arg(long, help_heading = "Rule configuration", hide = true)]
pub dummy_variable_rgx: Option<Regex>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>,
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(
short,
long,
help_heading = "Miscellaneous",
conflicts_with = "exit_non_zero_on_fix"
)]
pub exit_zero: bool,
/// Exit with a non-zero status code if any files were modified via fix, even if no lint violations remain.
#[arg(long, help_heading = "Miscellaneous", conflicts_with = "exit_zero")]
pub exit_non_zero_on_fix: bool,
/// Show counts for every rule with at least one violation.
#[arg(
long,
// Unsupported default-command arguments.
conflicts_with = "diff",
conflicts_with = "show_source",
conflicts_with = "watch",
)]
pub statistics: bool,
/// Enable automatic additions of `noqa` directives to failing lines.
#[arg(
long,
// conflicts_with = "add_noqa",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "ignore_noqa",
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
conflicts_with = "fix",
)]
pub add_noqa: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
// conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "ignore_noqa",
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub show_files: bool,
/// See the settings Ruff will use to lint a given Python file.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "show_files",
// conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "ignore_noqa",
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub show_settings: bool,
/// Dev-only argument to show fixes
#[arg(long, hide = true)]
pub ecosystem_ci: bool,
}
#[derive(Clone, Debug, clap::Parser)]
#[allow(clippy::struct_excessive_bools)]
pub struct FormatCommand {
/// List of files or directories to format.
pub files: Vec<PathBuf>,
/// Avoid writing any formatted files back; instead, exit with a non-zero status code if any
/// files would have been modified, and zero otherwise.
#[arg(long)]
pub check: bool,
/// Avoid writing any formatted files back; instead, exit with a non-zero status code and the
/// difference between the current file and how the formatted file would look like.
#[arg(long)]
pub diff: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Disable cache reads.
#[arg(short, long, help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Path to the cache directory.
#[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")]
pub cache_dir: Option<PathBuf>,
/// Respect file exclusions via `.gitignore` and other standard ignore files.
/// Use `--no-respect-gitignore` to disable.
#[arg(
long,
overrides_with("no_respect_gitignore"),
help_heading = "File selection"
)]
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
/// Use `--no-force-exclude` to disable.
#[arg(
long,
overrides_with("no_force_exclude"),
help_heading = "File selection"
)]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// Set the line-length.
#[arg(long, help_heading = "Format configuration")]
pub line_length: Option<LineLength>,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config", help_heading = "Miscellaneous")]
pub isolated: bool,
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
pub target_version: Option<PythonVersion>,
/// Enable preview mode; enables unstable formatting.
/// Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HelpFormat {
Text,
Json,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub verbose: bool,
/// Print diagnostics, but nothing else.
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting diagnostics).
#[arg(
short,
long,
global = true,
group = "verbosity",
help_heading = "Log levels"
)]
pub silent: bool,
}
impl From<&LogLevelArgs> for LogLevel {
fn from(args: &LogLevelArgs) -> Self {
if args.silent {
Self::Silent
} else if args.quiet {
Self::Quiet
} else if args.verbose {
Self::Verbose
} else {
Self::Default
}
}
}
impl CheckCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> (CheckArguments, CliOverrides) {
(
CheckArguments {
add_noqa: self.add_noqa,
config: self.config,
diff: self.diff,
ecosystem_ci: self.ecosystem_ci,
exit_non_zero_on_fix: self.exit_non_zero_on_fix,
exit_zero: self.exit_zero,
files: self.files,
ignore_noqa: self.ignore_noqa,
isolated: self.isolated,
no_cache: self.no_cache,
output_file: self.output_file,
show_files: self.show_files,
show_settings: self.show_settings,
statistics: self.statistics,
stdin_filename: self.stdin_filename,
watch: self.watch,
},
CliOverrides {
dummy_variable_rgx: self.dummy_variable_rgx,
exclude: self.exclude,
extend_exclude: self.extend_exclude,
extend_fixable: self.extend_fixable,
extend_ignore: self.extend_ignore,
extend_per_file_ignores: self.extend_per_file_ignores,
extend_select: self.extend_select,
extend_unfixable: self.extend_unfixable,
fixable: self.fixable,
ignore: self.ignore,
line_length: self.line_length,
per_file_ignores: self.per_file_ignores,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,
),
select: self.select,
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
target_version: self.target_version,
unfixable: self.unfixable,
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
cache_dir: self.cache_dir,
fix: resolve_bool_arg(self.fix, self.no_fix),
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
unsafe_fixes: resolve_bool_arg(self.unsafe_fixes, self.no_unsafe_fixes)
.map(UnsafeFixes::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
output_format: self.output_format,
show_fixes: resolve_bool_arg(self.show_fixes, self.no_show_fixes),
},
)
}
}
impl FormatCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> (FormatArguments, CliOverrides) {
(
FormatArguments {
check: self.check,
diff: self.diff,
config: self.config,
files: self.files,
isolated: self.isolated,
no_cache: self.no_cache,
stdin_filename: self.stdin_filename,
},
CliOverrides {
line_length: self.line_length,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,
),
exclude: self.exclude,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
target_version: self.target_version,
cache_dir: self.cache_dir,
// Unsupported on the formatter CLI, but required on `Overrides`.
..CliOverrides::default()
},
)
}
}
fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
match (yes, no) {
(true, false) => Some(true),
(false, true) => Some(false),
(false, false) => None,
(..) => unreachable!("Clap should make this impossible"),
}
}
/// CLI settings that are distinct from configuration (commands, lists of files,
/// etc.).
#[allow(clippy::struct_excessive_bools)]
pub struct CheckArguments {
pub add_noqa: bool,
pub config: Option<PathBuf>,
pub diff: bool,
pub ecosystem_ci: bool,
pub exit_non_zero_on_fix: bool,
pub exit_zero: bool,
pub files: Vec<PathBuf>,
pub ignore_noqa: bool,
pub isolated: bool,
pub no_cache: bool,
pub output_file: Option<PathBuf>,
pub show_files: bool,
pub show_settings: bool,
pub statistics: bool,
pub stdin_filename: Option<PathBuf>,
pub watch: bool,
}
/// CLI settings that are distinct from configuration (commands, lists of files,
/// etc.).
#[allow(clippy::struct_excessive_bools)]
pub struct FormatArguments {
pub check: bool,
pub no_cache: bool,
pub diff: bool,
pub config: Option<PathBuf>,
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
}
/// CLI settings that function as configuration overrides.
#[derive(Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct CliOverrides {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_fixable: Option<Vec<RuleSelector>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
pub extend_select: Option<Vec<RuleSelector>>,
pub extend_unfixable: Option<Vec<RuleSelector>>,
pub fixable: Option<Vec<RuleSelector>>,
pub ignore: Option<Vec<RuleSelector>>,
pub line_length: Option<LineLength>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub extend_per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub preview: Option<PreviewMode>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<RuleSelector>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<RuleSelector>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
pub unsafe_fixes: Option<UnsafeFixes>,
pub force_exclude: Option<bool>,
pub output_format: Option<SerializationFormat>,
pub show_fixes: Option<bool>,
}
impl ConfigurationTransformer for CliOverrides {
fn transform(&self, mut config: Configuration) -> Configuration {
if let Some(cache_dir) = &self.cache_dir {
config.cache_dir = Some(cache_dir.clone());
}
if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx {
config.lint.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
}
if let Some(exclude) = &self.exclude {
config.exclude = Some(exclude.clone());
}
if let Some(extend_exclude) = &self.extend_exclude {
config.extend_exclude.extend(extend_exclude.clone());
}
if let Some(extend_per_file_ignores) = &self.extend_per_file_ignores {
config
.lint
.extend_per_file_ignores
.extend(collect_per_file_ignores(extend_per_file_ignores.clone()));
}
if let Some(fix) = &self.fix {
config.fix = Some(*fix);
}
if let Some(fix_only) = &self.fix_only {
config.fix_only = Some(*fix_only);
}
if self.unsafe_fixes.is_some() {
config.unsafe_fixes = self.unsafe_fixes;
}
config.lint.rule_selections.push(RuleSelection {
select: self.select.clone(),
ignore: self
.ignore
.iter()
.cloned()
.chain(self.extend_ignore.iter().cloned())
.flatten()
.collect(),
extend_select: self.extend_select.clone().unwrap_or_default(),
fixable: self.fixable.clone(),
unfixable: self
.unfixable
.iter()
.cloned()
.chain(self.extend_unfixable.iter().cloned())
.flatten()
.collect(),
extend_fixable: self.extend_fixable.clone().unwrap_or_default(),
});
if let Some(output_format) = &self.output_format {
config.output_format = Some(*output_format);
}
if let Some(force_exclude) = &self.force_exclude {
config.force_exclude = Some(*force_exclude);
}
if let Some(line_length) = self.line_length {
config.line_length = Some(line_length);
config.lint.pycodestyle = Some(PycodestyleOptions {
max_line_length: Some(line_length),
..config.lint.pycodestyle.unwrap_or_default()
});
}
if let Some(preview) = &self.preview {
config.preview = Some(*preview);
config.lint.preview = Some(*preview);
config.format.preview = Some(*preview);
}
if let Some(per_file_ignores) = &self.per_file_ignores {
config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
}
if let Some(respect_gitignore) = &self.respect_gitignore {
config.respect_gitignore = Some(*respect_gitignore);
}
if let Some(show_source) = &self.show_source {
config.show_source = Some(*show_source);
}
if let Some(show_fixes) = &self.show_fixes {
config.show_fixes = Some(*show_fixes);
}
if let Some(target_version) = &self.target_version {
config.target_version = Some(*target_version);
}
config
}
}
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)
.or_default()
.push(pair.prefix);
}
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes, None))
.collect()
}

View File

@@ -3,8 +3,8 @@ use std::process::ExitCode;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use colored::Colorize; use colored::Colorize;
use ruff::args::{Args, Command}; use ruff_cli::args::{Args, Command};
use ruff::{run, ExitStatus}; use ruff_cli::{run, ExitStatus};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[global_allocator] #[global_allocator]
@@ -27,42 +27,26 @@ pub fn main() -> ExitCode {
let mut args = let mut args =
argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap(); argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX).unwrap();
// We can't use `warn_user` here because logging isn't set up at this point // Clap doesn't support default subcommands but we want to run `check` by
// and we also don't know if the user runs ruff with quiet. // default for convenience and backwards-compatibility, so we just
// Keep the message and pass it to `run` that is responsible for emitting the warning. // preprocess the arguments accordingly before passing them to Clap.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) { if let Some(arg) = args.get(1) {
// Deprecated aliases that are handled by clap if arg
Some("--explain") => { .to_str()
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.") .is_some_and(|arg| !Command::has_subcommand(rewrite_legacy_subcommand(arg)))
}
Some("--clean") => {
Some("`ruff --clean` is deprecated. Use `ruff clean` instead.")
}
Some("--generate-shell-completion") => {
Some("`ruff --generate-shell-completion <SHELL>` is deprecated. Use `ruff generate-shell-completion <SHELL>` instead.")
}
// Deprecated `ruff` alias to `ruff check`
// Clap doesn't support default subcommands but we want to run `check` by
// default for convenience and backwards-compatibility, so we just
// preprocess the arguments accordingly before passing them to Clap.
Some(arg) if !Command::has_subcommand(arg)
&& arg != "-h" && arg != "-h"
&& arg != "--help" && arg != "--help"
&& arg != "-V" && arg != "-V"
&& arg != "--version" && arg != "--version"
&& arg != "help" => { && arg != "help"
{
{ args.insert(1, "check".into());
args.insert(1, "check".into()); }
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.") }
}
},
_ => None
};
let args = Args::parse_from(args); let args = Args::parse_from(args);
match run(args, deprecated_alias_warning) { match run(args) {
Ok(code) => code.into(), Ok(code) => code.into(),
Err(err) => { Err(err) => {
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
@@ -81,3 +65,12 @@ pub fn main() -> ExitCode {
} }
} }
} }
fn rewrite_legacy_subcommand(cmd: &str) -> &str {
match cmd {
"--explain" => "rule",
"--clean" => "clean",
"--generate-shell-completion" => "generate-shell-completion",
cmd => cmd,
}
}

View File

@@ -1,7 +1,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::{self, File}; use std::fs::{self, File};
use std::hash::Hasher; use std::hash::Hasher;
use std::io::{self, BufReader, Write}; use std::io::{self, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
@@ -15,7 +15,6 @@ use rayon::iter::ParallelIterator;
use rayon::iter::{IntoParallelIterator, ParallelBridge}; use rayon::iter::{IntoParallelIterator, ParallelBridge};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_diagnostics::{DiagnosticKind, Fix}; use ruff_diagnostics::{DiagnosticKind, Fix};
@@ -26,9 +25,10 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap; use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder; use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::Resolver; use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics; use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`]. /// [`Path`] that is relative to the package root in [`PackageCache`].
@@ -86,7 +86,6 @@ pub(crate) struct Cache {
changes: Mutex<Vec<Change>>, changes: Mutex<Vec<Change>>,
/// The "current" timestamp used as cache for the updates of /// The "current" timestamp used as cache for the updates of
/// [`FileCache::last_seen`] /// [`FileCache::last_seen`]
#[allow(clippy::struct_field_names)]
last_seen_cache: u64, last_seen_cache: u64,
} }
@@ -166,29 +165,15 @@ impl Cache {
return Ok(()); return Ok(());
} }
// Write the cache to a temporary file first and then rename it for an "atomic" write. let file = File::create(&self.path)
// Protects against data loss if the process is killed during the write and races between different ruff .with_context(|| format!("Failed to create cache file '{}'", self.path.display()))?;
// processes, resulting in a corrupted cache file. https://github.com/astral-sh/ruff/issues/8147#issuecomment-1943345964 let writer = BufWriter::new(file);
let mut temp_file = bincode::serialize_into(writer, &self.package).with_context(|| {
NamedTempFile::new_in(self.path.parent().expect("Write path must have a parent"))
.context("Failed to create temporary file")?;
// Serialize to in-memory buffer because hyperfine benchmark showed that it's faster than
// using a `BufWriter` and our cache files are small enough that streaming isn't necessary.
let serialized =
bincode::serialize(&self.package).context("Failed to serialize cache data")?;
temp_file
.write_all(&serialized)
.context("Failed to write serialized cache to temporary file.")?;
temp_file.persist(&self.path).with_context(|| {
format!( format!(
"Failed to rename temporary cache file to {}", "Failed to serialise cache to file '{}'",
self.path.display() self.path.display()
) )
})?; })
Ok(())
} }
/// Applies the pending changes without storing the cache to disk. /// Applies the pending changes without storing the cache to disk.
@@ -383,7 +368,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
let gitignore_path = path.join(".gitignore"); let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() { if !gitignore_path.exists() {
let mut file = fs::File::create(gitignore_path)?; let mut file = fs::File::create(gitignore_path)?;
file.write_all(b"# Automatically created by ruff.\n*\n")?; file.write_all(b"*")?;
} }
Ok(()) Ok(())
@@ -457,7 +442,7 @@ pub(super) struct CacheMessage {
pub(crate) trait PackageCaches { pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>; fn get(&self, package_root: &Path) -> Option<&Cache>;
fn persist(self) -> Result<()>; fn persist(self) -> anyhow::Result<()>;
} }
impl<T> PackageCaches for Option<T> impl<T> PackageCaches for Option<T>
@@ -483,17 +468,27 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
impl<'a> PackageCacheMap<'a> { impl<'a> PackageCacheMap<'a> {
pub(crate) fn init( pub(crate) fn init(
pyproject_config: &PyprojectConfig,
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>, package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
resolver: &Resolver, resolver: &Resolver,
) -> Self { ) -> Self {
fn init_cache(path: &Path) { fn init_cache(path: &Path) {
if let Err(e) = init(path) { if let Err(e) = cache::init(path) {
error!("Failed to initialize cache at {}: {e:?}", path.display()); error!("Failed to initialize cache at {}: {e:?}", path.display());
} }
} }
for settings in resolver.settings() { match pyproject_config.strategy {
init_cache(&settings.cache_dir); PyprojectDiscoveryStrategy::Fixed => {
init_cache(&pyproject_config.settings.cache_dir);
}
PyprojectDiscoveryStrategy::Hierarchical => {
for settings in
std::iter::once(&pyproject_config.settings).chain(resolver.settings())
{
init_cache(&settings.cache_dir);
}
}
} }
Self( Self(
@@ -503,7 +498,7 @@ impl<'a> PackageCacheMap<'a> {
.unique() .unique()
.par_bridge() .par_bridge()
.map(|cache_root| { .map(|cache_root| {
let settings = resolver.resolve(cache_root); let settings = resolver.resolve(cache_root, pyproject_config);
let cache = Cache::open(cache_root.to_path_buf(), settings); let cache = Cache::open(cache_root.to_path_buf(), settings);
(cache_root, cache) (cache_root, cache)
}) })
@@ -1065,7 +1060,6 @@ mod tests {
&self.settings.formatter, &self.settings.formatter,
PySourceType::Python, PySourceType::Python,
FormatMode::Write, FormatMode::Write,
None,
Some(cache), Some(cache),
) )
} }

View File

@@ -12,17 +12,17 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Add `noqa` directives to a collection of files. /// Add `noqa` directives to a collection of files.
pub(crate) fn add_noqa( pub(crate) fn add_noqa(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
) -> Result<usize> { ) -> Result<usize> {
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
@@ -38,6 +38,7 @@ pub(crate) fn add_noqa(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
pyproject_config,
); );
let start = Instant::now(); let start = Instant::now();
@@ -56,7 +57,7 @@ pub(crate) fn add_noqa(
.parent() .parent()
.and_then(|parent| package_roots.get(parent)) .and_then(|parent| package_roots.get(parent))
.and_then(|package| *package); .and_then(|package| *package);
let settings = resolver.resolve(path); let settings = resolver.resolve(path, pyproject_config);
let source_kind = match SourceKind::from_path(path, source_type) { let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind, Ok(Some(source_kind)) => source_kind,
Ok(None) => return None, Ok(None) => return None,

View File

@@ -24,17 +24,16 @@ use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile,
}; };
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use crate::cache::{Cache, PackageCacheMap, PackageCaches}; use crate::cache::{Cache, PackageCacheMap, PackageCaches};
use crate::diagnostics::Diagnostics; use crate::diagnostics::Diagnostics;
use crate::panic::catch_unwind; use crate::panic::catch_unwind;
/// Run the linter over a collection of files. /// Run the linter over a collection of files.
#[allow(clippy::too_many_arguments)]
pub(crate) fn check( pub(crate) fn check(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
cache: flags::Cache, cache: flags::Cache,
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
@@ -42,7 +41,7 @@ pub(crate) fn check(
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
// Collect all the Python files to check. // Collect all the Python files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
debug!("Identified files to lint in: {:?}", start.elapsed()); debug!("Identified files to lint in: {:?}", start.elapsed());
if paths.is_empty() { if paths.is_empty() {
@@ -57,11 +56,16 @@ pub(crate) fn check(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
pyproject_config,
); );
// Load the caches. // Load the caches.
let caches = if bool::from(cache) { let caches = if bool::from(cache) {
Some(PackageCacheMap::init(&package_roots, &resolver)) Some(PackageCacheMap::init(
pyproject_config,
&package_roots,
&resolver,
))
} else { } else {
None None
}; };
@@ -76,7 +80,7 @@ pub(crate) fn check(
.and_then(|parent| package_roots.get(parent)) .and_then(|parent| package_roots.get(parent))
.and_then(|package| *package); .and_then(|package| *package);
let settings = resolver.resolve(path); let settings = resolver.resolve(path, pyproject_config);
if (settings.file_resolver.force_exclude || !resolved_file.is_root()) if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion( && match_exclusion(
@@ -123,7 +127,7 @@ pub(crate) fn check(
Some(result.unwrap_or_else(|(path, message)| { Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path { if let Some(path) = &path {
let settings = resolver.resolve(path); let settings = resolver.resolve(path, pyproject_config);
if settings.linter.rules.enabled(Rule::IOError) { if settings.linter.rules.enabled(Rule::IOError) {
let dummy = let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
@@ -180,7 +184,6 @@ pub(crate) fn check(
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits /// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits
/// a diagnostic if the linting the file panics. /// a diagnostic if the linting the file panics.
#[allow(clippy::too_many_arguments)]
fn lint_path( fn lint_path(
path: &Path, path: &Path,
package: Option<&Path>, package: Option<&Path>,
@@ -197,12 +200,12 @@ fn lint_path(
match result { match result {
Ok(inner) => inner, Ok(inner) => inner,
Err(error) => { Err(error) => {
let message = r"This indicates a bug in Ruff. If you could open an issue at: let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative! ...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"; "#;
error!( error!(
"{}{}{} {message}\n{error}", "{}{}{} {message}\n{error}",
@@ -233,7 +236,7 @@ mod test {
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy}; use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use super::check; use super::check;
@@ -272,7 +275,7 @@ mod test {
// Notebooks are not included by default // Notebooks are not included by default
&[tempdir.path().to_path_buf(), notebook], &[tempdir.path().to_path_buf(), notebook],
&pyproject_config, &pyproject_config,
&ConfigArguments::default(), &CliOverrides::default(),
flags::Cache::Disabled, flags::Cache::Disabled,
flags::Noqa::Disabled, flags::Noqa::Disabled,
flags::FixMode::Generate, flags::FixMode::Generate,

View File

@@ -4,50 +4,44 @@ use anyhow::Result;
use ruff_linter::packaging; use ruff_linter::packaging;
use ruff_linter::settings::flags; use ruff_linter::settings::flags;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver}; use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics}; use crate::diagnostics::{lint_stdin, Diagnostics};
use crate::stdin::{parrot_stdin, read_from_stdin}; use crate::stdin::read_from_stdin;
/// Run the linter over a single file, read from `stdin`. /// Run the linter over a single file, read from `stdin`.
pub(crate) fn check_stdin( pub(crate) fn check_stdin(
filename: Option<&Path>, filename: Option<&Path>,
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
overrides: &ConfigArguments, overrides: &CliOverrides,
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
let mut resolver = Resolver::new(pyproject_config); if pyproject_config.settings.file_resolver.force_exclude {
if resolver.force_exclude() {
if let Some(filename) = filename { if let Some(filename) = filename {
if !python_file_at_path(filename, &mut resolver, overrides)? { if !python_file_at_path(filename, pyproject_config, overrides)? {
if fix_mode.is_apply() {
parrot_stdin()?;
}
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
if filename.file_name().is_some_and(|name| { let lint_settings = &pyproject_config.settings.linter;
match_exclusion(filename, name, &resolver.base_settings().linter.exclude) if filename
}) { .file_name()
if fix_mode.is_apply() { .is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
parrot_stdin()?; {
}
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
} }
} }
let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| { let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages) packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
}); });
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin( let mut diagnostics = lint_stdin(
filename, filename,
package_root, package_root,
stdin, stdin,
resolver.base_settings(), &pyproject_config.settings,
noqa, noqa,
fix_mode, fix_mode,
)?; )?;

View File

@@ -17,23 +17,24 @@ use tracing::debug;
use ruff_diagnostics::SourceMap; use ruff_diagnostics::SourceMap;
use ruff_linter::fs; use ruff_linter::fs;
use ruff_linter::logging::{DisplayParseError, LogLevel}; use ruff_linter::logging::LogLevel;
use ruff_linter::registry::Rule; use ruff_linter::registry::Rule;
use ruff_linter::rules::flake8_quotes::settings::Quote; use ruff_linter::rules::flake8_quotes::settings::Quote;
use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::warn_user_once; use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, format_range, FormatModuleError, QuoteStyle}; use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_source_file::LineIndex;
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver}; use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
};
use ruff_workspace::FormatterSettings; use ruff_workspace::FormatterSettings;
use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::args::{CliOverrides, FormatArguments};
use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches}; use crate::cache::{Cache, FileCacheKey, PackageCacheMap, PackageCaches};
use crate::panic::{catch_unwind, PanicError}; use crate::panic::{catch_unwind, PanicError};
use crate::resolve::resolve; use crate::resolve::resolve;
use crate::{resolve_default_files, ExitStatus}; use crate::ExitStatus;
#[derive(Debug, Copy, Clone, is_macro::Is)] #[derive(Debug, Copy, Clone, is_macro::Is)]
pub(crate) enum FormatMode { pub(crate) enum FormatMode {
@@ -59,27 +60,25 @@ impl FormatMode {
/// Format a set of files, and return the exit status. /// Format a set of files, and return the exit status.
pub(crate) fn format( pub(crate) fn format(
cli: FormatArguments, cli: &FormatArguments,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
log_level: LogLevel,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?; let pyproject_config = resolve(
let mode = FormatMode::from_cli(&cli); cli.isolated,
let files = resolve_default_files(cli.files, false); cli.config.as_deref(),
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?; overrides,
cli.stdin_filename.as_deref(),
)?;
let mode = FormatMode::from_cli(cli);
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?;
if paths.is_empty() { if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)"); warn_user_once!("No Python files found under the given path(s)");
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
if cli.range.is_some() && paths.len() > 1 { warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver));
return Err(anyhow::anyhow!(
"The `--range` option is only supported when formatting a single file but the specified paths resolve to {} files.",
paths.len()
));
}
warn_incompatible_formatter_settings(&resolver);
// Discover the package root for each Python file. // Discover the package root for each Python file.
let package_roots = resolver.package_roots( let package_roots = resolver.package_roots(
@@ -88,6 +87,7 @@ pub(crate) fn format(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&pyproject_config,
); );
let caches = if cli.no_cache { let caches = if cli.no_cache {
@@ -98,7 +98,11 @@ pub(crate) fn format(
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
crate::warn_user!("Detected debug build without --no-cache."); crate::warn_user!("Detected debug build without --no-cache.");
Some(PackageCacheMap::init(&package_roots, &resolver)) Some(PackageCacheMap::init(
&pyproject_config,
&package_roots,
&resolver,
))
}; };
let start = Instant::now(); let start = Instant::now();
@@ -108,19 +112,13 @@ pub(crate) fn format(
match entry { match entry {
Ok(resolved_file) => { Ok(resolved_file) => {
let path = resolved_file.path(); let path = resolved_file.path();
let settings = resolver.resolve(path); let SourceType::Python(source_type) = SourceType::from(&path) else {
// Ignore any non-Python files.
let source_type = match settings.formatter.extension.get(path) { return None;
None => match SourceType::from(path) {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
// Ignore any non-Python files.
return None;
}
},
Some(language) => PySourceType::from(language),
}; };
let settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting // Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root()) if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion( && match_exclusion(
@@ -141,14 +139,7 @@ pub(crate) fn format(
Some( Some(
match catch_unwind(|| { match catch_unwind(|| {
format_path( format_path(path, &settings.formatter, source_type, mode, cache)
path,
&settings.formatter,
source_type,
mode,
cli.range,
cache,
)
}) { }) {
Ok(inner) => inner.map(|result| FormatPathResult { Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.path().to_path_buf(), path: resolved_file.path().to_path_buf(),
@@ -197,7 +188,7 @@ pub(crate) fn format(
} }
// Report on the formatting changes. // Report on the formatting changes.
if config_arguments.log_level >= LogLevel::Default { if log_level >= LogLevel::Default {
if mode.is_diff() { if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr // Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?; results.write_summary(&mut stderr().lock())?;
@@ -235,7 +226,6 @@ pub(crate) fn format_path(
settings: &FormatterSettings, settings: &FormatterSettings,
source_type: PySourceType, source_type: PySourceType,
mode: FormatMode, mode: FormatMode,
range: Option<FormatRange>,
cache: Option<&Cache>, cache: Option<&Cache>,
) -> Result<FormatResult, FormatCommandError> { ) -> Result<FormatResult, FormatCommandError> {
if let Some(cache) = cache { if let Some(cache) = cache {
@@ -253,19 +243,15 @@ pub(crate) fn format_path(
// Extract the sources from the file. // Extract the sources from the file.
let unformatted = match SourceKind::from_path(path, source_type) { let unformatted = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind, Ok(Some(source_kind)) => source_kind,
// Non-Python Jupyter notebook. // Non Python Jupyter notebook
Ok(None) => return Ok(FormatResult::Skipped), Ok(None) => return Ok(FormatResult::Skipped),
Err(err) => { Err(err) => {
return Err(FormatCommandError::Read(Some(path.to_path_buf()), err)); return Err(FormatCommandError::Read(Some(path.to_path_buf()), err));
} }
}; };
// Don't write back to the cache if formatting a range.
let cache = cache.filter(|_| range.is_none());
// Format the source. // Format the source.
let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)? let format_result = match format_source(&unformatted, source_type, Some(path), settings)? {
{
FormattedSource::Formatted(formatted) => match mode { FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => { FormatMode::Write => {
let mut writer = File::create(path).map_err(|err| { let mut writer = File::create(path).map_err(|err| {
@@ -333,43 +319,15 @@ pub(crate) fn format_source(
source_type: PySourceType, source_type: PySourceType,
path: Option<&Path>, path: Option<&Path>,
settings: &FormatterSettings, settings: &FormatterSettings,
range: Option<FormatRange>,
) -> Result<FormattedSource, FormatCommandError> { ) -> Result<FormattedSource, FormatCommandError> {
match &source_kind { match source_kind {
SourceKind::Python(unformatted) => { SourceKind::Python(unformatted) => {
let options = settings.to_format_options(source_type, unformatted); let options = settings.to_format_options(source_type, unformatted);
let formatted = if let Some(range) = range { let formatted = format_module_source(unformatted, options)
let line_index = LineIndex::from_source_text(unformatted); .map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?;
let byte_range = range.to_text_range(unformatted, &line_index);
format_range(unformatted, byte_range, options).map(|formatted_range| {
let mut formatted = unformatted.to_string();
formatted.replace_range(
std::ops::Range::<usize>::from(formatted_range.source_range()),
formatted_range.as_code(),
);
formatted
})
} else {
// Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless.
#[allow(clippy::redundant_closure_for_method_calls)]
format_module_source(unformatted, options).map(|formatted| formatted.into_code())
};
let formatted = formatted.map_err(|err| {
if let FormatModuleError::ParseError(err) = err {
DisplayParseError::from_source_kind(
err,
path.map(Path::to_path_buf),
source_kind,
)
.into()
} else {
FormatCommandError::Format(path.map(Path::to_path_buf), err)
}
})?;
let formatted = formatted.into_code();
if formatted.len() == unformatted.len() && formatted == *unformatted { if formatted.len() == unformatted.len() && formatted == *unformatted {
Ok(FormattedSource::Unchanged) Ok(FormattedSource::Unchanged)
} else { } else {
@@ -381,12 +339,6 @@ pub(crate) fn format_source(
return Ok(FormattedSource::Unchanged); return Ok(FormattedSource::Unchanged);
} }
if range.is_some() {
return Err(FormatCommandError::RangeFormatNotebook(
path.map(Path::to_path_buf),
));
}
let options = settings.to_format_options(source_type, notebook.source_code()); let options = settings.to_format_options(source_type, notebook.source_code());
let mut output: Option<String> = None; let mut output: Option<String> = None;
@@ -399,19 +351,8 @@ pub(crate) fn format_source(
let unformatted = &notebook.source_code()[range]; let unformatted = &notebook.source_code()[range];
// Format the cell. // Format the cell.
let formatted = let formatted = format_module_source(unformatted, options.clone())
format_module_source(unformatted, options.clone()).map_err(|err| { .map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?;
if let FormatModuleError::ParseError(err) = err {
DisplayParseError::from_source_kind(
err,
path.map(Path::to_path_buf),
source_kind,
)
.into()
} else {
FormatCommandError::Format(path.map(Path::to_path_buf), err)
}
})?;
// If the cell is unchanged, skip it. // If the cell is unchanged, skip it.
let formatted = formatted.as_code(); let formatted = formatted.as_code();
@@ -466,13 +407,11 @@ pub(crate) fn format_source(
pub(crate) enum FormatResult { pub(crate) enum FormatResult {
/// The file was formatted. /// The file was formatted.
Formatted, Formatted,
/// The file was formatted, [`SourceKind`] contains the formatted code /// The file was formatted, [`SourceKind`] contains the formatted code
Diff { Diff {
unformatted: SourceKind, unformatted: SourceKind,
formatted: SourceKind, formatted: SourceKind,
}, },
/// The file was unchanged, as the formatted contents matched the existing contents. /// The file was unchanged, as the formatted contents matched the existing contents.
Unchanged, Unchanged,
@@ -527,7 +466,7 @@ impl<'a> FormatResults<'a> {
}) })
.sorted_unstable_by_key(|(path, _, _)| *path) .sorted_unstable_by_key(|(path, _, _)| *path)
{ {
write!(f, "{}", unformatted.diff(formatted, Some(path)).unwrap())?; unformatted.diff(formatted, Some(path), f)?;
} }
Ok(()) Ok(())
@@ -575,7 +514,7 @@ impl<'a> FormatResults<'a> {
if changed > 0 && unchanged > 0 { if changed > 0 && unchanged > 0 {
writeln!( writeln!(
f, f,
"{} file{} {}, {} file{} {}", "{} file{} {}, {} file{} left unchanged",
changed, changed,
if changed == 1 { "" } else { "s" }, if changed == 1 { "" } else { "s" },
match self.mode { match self.mode {
@@ -584,10 +523,6 @@ impl<'a> FormatResults<'a> {
}, },
unchanged, unchanged,
if unchanged == 1 { "" } else { "s" }, if unchanged == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "left unchanged",
FormatMode::Check | FormatMode::Diff => "already formatted",
},
) )
} else if changed > 0 { } else if changed > 0 {
writeln!( writeln!(
@@ -603,13 +538,9 @@ impl<'a> FormatResults<'a> {
} else if unchanged > 0 { } else if unchanged > 0 {
writeln!( writeln!(
f, f,
"{} file{} {}", "{} file{} left unchanged",
unchanged, unchanged,
if unchanged == 1 { "" } else { "s" }, if unchanged == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "left unchanged",
FormatMode::Check | FormatMode::Diff => "already formatted",
},
) )
} else { } else {
Ok(()) Ok(())
@@ -621,13 +552,11 @@ impl<'a> FormatResults<'a> {
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub(crate) enum FormatCommandError { pub(crate) enum FormatCommandError {
Ignore(#[from] ignore::Error), Ignore(#[from] ignore::Error),
Parse(#[from] DisplayParseError),
Panic(Option<PathBuf>, PanicError), Panic(Option<PathBuf>, PanicError),
Read(Option<PathBuf>, SourceError), Read(Option<PathBuf>, SourceError),
Format(Option<PathBuf>, FormatModuleError), Format(Option<PathBuf>, FormatModuleError),
Write(Option<PathBuf>, SourceError), Write(Option<PathBuf>, SourceError),
Diff(Option<PathBuf>, io::Error), Diff(Option<PathBuf>, io::Error),
RangeFormatNotebook(Option<PathBuf>),
} }
impl FormatCommandError { impl FormatCommandError {
@@ -640,13 +569,11 @@ impl FormatCommandError {
None None
} }
} }
Self::Parse(err) => err.path(),
Self::Panic(path, _) Self::Panic(path, _)
| Self::Read(path, _) | Self::Read(path, _)
| Self::Format(path, _) | Self::Format(path, _)
| Self::Write(path, _) | Self::Write(path, _)
| Self::Diff(path, _) | Self::Diff(path, _) => path.as_deref(),
| Self::RangeFormatNotebook(path) => path.as_deref(),
} }
} }
} }
@@ -668,17 +595,13 @@ impl Display for FormatCommandError {
} else { } else {
write!( write!(
f, f,
"{header} {error}", "{} {}",
header = "Encountered error:".bold(), "Encountered error:".bold(),
error = err err.io_error()
.io_error()
.map_or_else(|| err.to_string(), std::string::ToString::to_string) .map_or_else(|| err.to_string(), std::string::ToString::to_string)
) )
} }
} }
Self::Parse(err) => {
write!(f, "{err}")
}
Self::Read(path, err) => { Self::Read(path, err) => {
if let Some(path) = path { if let Some(path) = path {
write!( write!(
@@ -689,7 +612,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to read:".bold()) write!(f, "{}{} {err}", "Failed to read".bold(), ":".bold())
} }
} }
Self::Write(path, err) => { Self::Write(path, err) => {
@@ -702,7 +625,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to write:".bold()) write!(f, "{}{} {err}", "Failed to write".bold(), ":".bold())
} }
} }
Self::Format(path, err) => { Self::Format(path, err) => {
@@ -715,7 +638,7 @@ impl Display for FormatCommandError {
":".bold() ":".bold()
) )
} else { } else {
write!(f, "{header} {err}", header = "Failed to format:".bold()) write!(f, "{}{} {err}", "Failed to format".bold(), ":".bold())
} }
} }
Self::Diff(path, err) => { Self::Diff(path, err) => {
@@ -730,35 +653,19 @@ impl Display for FormatCommandError {
} else { } else {
write!( write!(
f, f,
"{header} {err}", "{}{} {err}",
header = "Failed to generate diff:".bold(), "Failed to generate diff".bold(),
) ":".bold()
}
}
Self::RangeFormatNotebook(path) => {
if let Some(path) = path {
write!(
f,
"{header}{path}{colon} Range formatting isn't supported for notebooks.",
header = "Failed to format ".bold(),
path = fs::relativize_path(path).bold(),
colon = ":".bold()
)
} else {
write!(
f,
"{header} Range formatting isn't supported for notebooks",
header = "Failed to format:".bold()
) )
} }
} }
Self::Panic(path, err) => { Self::Panic(path, err) => {
let message = r"This indicates a bug in Ruff. If you could open an issue at: let message = r#"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BFormatter%20panic%5D https://github.com/astral-sh/ruff/issues/new?title=%5BFormatter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative! ...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
"; "#;
if let Some(path) = path { if let Some(path) = path {
write!( write!(
f, f,
@@ -779,10 +686,15 @@ impl Display for FormatCommandError {
} }
} }
pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { pub(super) fn warn_incompatible_formatter_settings(
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
// First, collect all rules that are incompatible regardless of the linter-specific settings. // First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default(); let mut incompatible_rules = FxHashSet::default();
for setting in resolver.settings() { for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for rule in [ for rule in [
// The formatter might collapse implicit string concatenation on a single line. // The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation, Rule::SingleLineImplicitStringConcatenation,
@@ -811,7 +723,9 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
} }
// Next, validate settings-specific incompatibilities. // Next, validate settings-specific incompatibilities.
for setting in resolver.settings() { for setting in std::iter::once(&pyproject_config.settings)
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
// Validate all rules that rely on tab styles. // Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation) if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab() && setting.formatter.indent_style.is_tab()

View File

@@ -6,68 +6,60 @@ use log::error;
use ruff_linter::source_kind::SourceKind; use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver}; use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::FormatterSettings; use ruff_workspace::FormatterSettings;
use crate::args::{ConfigArguments, FormatArguments, FormatRange}; use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{ use crate::commands::format::{
format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode, format_source, warn_incompatible_formatter_settings, FormatCommandError, FormatMode,
FormatResult, FormattedSource, FormatResult, FormattedSource,
}; };
use crate::resolve::resolve; use crate::resolve::resolve;
use crate::stdin::{parrot_stdin, read_from_stdin}; use crate::stdin::read_from_stdin;
use crate::ExitStatus; use crate::ExitStatus;
/// Run the formatter over a single file, read from `stdin`. /// Run the formatter over a single file, read from `stdin`.
pub(crate) fn format_stdin( pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> Result<ExitStatus> {
cli: &FormatArguments, let pyproject_config = resolve(
config_arguments: &ConfigArguments, cli.isolated,
) -> Result<ExitStatus> { cli.config.as_deref(),
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?; overrides,
cli.stdin_filename.as_deref(),
)?;
let mut resolver = Resolver::new(&pyproject_config); warn_incompatible_formatter_settings(&pyproject_config, None);
warn_incompatible_formatter_settings(&resolver);
let mode = FormatMode::from_cli(cli); let mode = FormatMode::from_cli(cli);
if resolver.force_exclude() { if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = cli.stdin_filename.as_deref() { if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &mut resolver, config_arguments)? { if !python_file_at_path(filename, &pyproject_config, overrides)? {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
if filename.file_name().is_some_and(|name| { let format_settings = &pyproject_config.settings.formatter;
match_exclusion(filename, name, &resolver.base_settings().formatter.exclude) if filename
}) { .file_name()
if mode.is_write() { .is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
parrot_stdin()?; {
}
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
} }
} }
let path = cli.stdin_filename.as_deref(); let path = cli.stdin_filename.as_deref();
let settings = &resolver.base_settings().formatter;
let source_type = match path.and_then(|path| settings.extension.get(path)) { let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
None => match path.map(SourceType::from).unwrap_or_default() { return Ok(ExitStatus::Success);
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
}
},
Some(language) => PySourceType::from(language),
}; };
// Format the file. // Format the file.
match format_source_code(path, cli.range, settings, source_type, mode) { match format_source_code(
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
Ok(result) => match mode { Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success), FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => { FormatMode::Check | FormatMode::Diff => {
@@ -88,7 +80,6 @@ pub(crate) fn format_stdin(
/// Format source code read from `stdin`. /// Format source code read from `stdin`.
fn format_source_code( fn format_source_code(
path: Option<&Path>, path: Option<&Path>,
range: Option<FormatRange>,
settings: &FormatterSettings, settings: &FormatterSettings,
source_type: PySourceType, source_type: PySourceType,
mode: FormatMode, mode: FormatMode,
@@ -106,7 +97,7 @@ fn format_source_code(
}; };
// Format the source. // Format the source.
let formatted = format_source(&source_kind, source_type, path, settings, range)?; let formatted = format_source(&source_kind, source_type, path, settings)?;
match &formatted { match &formatted {
FormattedSource::Formatted(formatted) => match mode { FormattedSource::Formatted(formatted) => match mode {
@@ -118,13 +109,9 @@ fn format_source_code(
} }
FormatMode::Check => {} FormatMode::Check => {}
FormatMode::Diff => { FormatMode::Diff => {
use std::io::Write; source_kind
write!( .diff(formatted, path, &mut stdout().lock())
&mut stdout().lock(), .map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
"{}",
source_kind.diff(formatted, path).unwrap()
)
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
} }
}, },
FormattedSource::Unchanged => { FormattedSource::Unchanged => {

View File

@@ -7,7 +7,6 @@ pub(crate) mod format;
pub(crate) mod format_stdin; pub(crate) mod format_stdin;
pub(crate) mod linter; pub(crate) mod linter;
pub(crate) mod rule; pub(crate) mod rule;
pub(crate) mod server;
pub(crate) mod show_files; pub(crate) mod show_files;
pub(crate) mod show_settings; pub(crate) mod show_settings;
pub(crate) mod version; pub(crate) mod version;

View File

@@ -18,7 +18,6 @@ struct Explanation<'a> {
summary: &'a str, summary: &'a str,
message_formats: &'a [&'a str], message_formats: &'a [&'a str],
fix: String, fix: String,
#[allow(clippy::struct_field_names)]
explanation: Option<&'a str>, explanation: Option<&'a str>,
preview: bool, preview: bool,
} }
@@ -64,7 +63,7 @@ fn format_rule_text(rule: Rule) -> String {
if rule.is_preview() || rule.is_nursery() { if rule.is_preview() || rule.is_nursery() {
output.push_str( output.push_str(
r"This rule is in preview and is not stable. The `--preview` flag is required for use.", r#"This rule is in preview and is not stable. The `--preview` flag is required for use."#,
); );
output.push('\n'); output.push('\n');
output.push('\n'); output.push('\n');

View File

@@ -7,17 +7,17 @@ use itertools::Itertools;
use ruff_linter::warn_user_once; use ruff_linter::warn_user_once;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Show the list of files to be checked based on current settings. /// Show the list of files to be checked based on current settings.
pub(crate) fn show_files( pub(crate) fn show_files(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
writer: &mut impl Write, writer: &mut impl Write,
) -> Result<()> { ) -> Result<()> {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, _resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, _resolver) = python_files_in_path(files, pyproject_config, overrides)?;
if paths.is_empty() { if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)"); warn_user_once!("No Python files found under the given path(s)");

View File

@@ -6,17 +6,17 @@ use itertools::Itertools;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile}; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Print the user-facing configuration settings. /// Print the user-facing configuration settings.
pub(crate) fn show_settings( pub(crate) fn show_settings(
files: &[PathBuf], files: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments, overrides: &CliOverrides,
writer: &mut impl Write, writer: &mut impl Write,
) -> Result<()> { ) -> Result<()> {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, resolver) = python_files_in_path(files, pyproject_config, config_arguments)?; let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
// Print the list of files. // Print the list of files.
let Some(path) = paths let Some(path) = paths
@@ -29,13 +29,13 @@ pub(crate) fn show_settings(
bail!("No files found under the given path"); bail!("No files found under the given path");
}; };
let settings = resolver.resolve(&path); let settings = resolver.resolve(&path, pyproject_config);
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?; writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() { if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?; writeln!(writer, "Settings path: {settings_path:?}")?;
} }
write!(writer, "{settings}")?; writeln!(writer, "{settings:#?}")?;
Ok(()) Ok(())
} }

View File

@@ -1,5 +1,5 @@
--- ---
source: crates/ruff/src/commands/check.rs source: crates/ruff_cli/src/commands/check.rs
--- ---
/home/ferris/project/code.py:1:1: E902 Permission denied (os error 13) /home/ferris/project/code.py:1:1: E902 Permission denied (os error 13)
/home/ferris/project/notebook.ipynb:1:1: E902 Permission denied (os error 13) /home/ferris/project/notebook.ipynb:1:1: E902 Permission denied (os error 13)

View File

@@ -1,9 +1,7 @@
#![cfg_attr(target_family = "wasm", allow(dead_code))] #![cfg_attr(target_family = "wasm", allow(dead_code))]
use std::borrow::Cow;
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::io::Write;
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign};
use std::path::Path; use std::path::Path;
@@ -12,8 +10,9 @@ use colored::Colorize;
use log::{debug, error, warn}; use log::{debug, error, warn};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::cache::{Cache, FileCacheKey, LintCacheData};
use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Diagnostic;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource}; use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
use ruff_linter::logging::DisplayParseError; use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message; use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml; use ruff_linter::pyproject_toml::lint_pyproject_toml;
@@ -24,13 +23,11 @@ use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{fs, IOError, SyntaxError}; use ruff_linter::{fs, IOError, SyntaxError};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex}; use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::imports::ImportMap; use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType}; use ruff_python_ast::{SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder; use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::cache::{Cache, FileCacheKey, LintCacheData};
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]
pub(crate) struct Diagnostics { pub(crate) struct Diagnostics {
pub(crate) messages: Vec<Message>, pub(crate) messages: Vec<Message>,
@@ -224,35 +221,31 @@ pub(crate) fn lint_path(
debug!("Checking: {}", path.display()); debug!("Checking: {}", path.display());
let source_type = match settings.extension.get(path).map(PySourceType::from) { let source_type = match SourceType::from(path) {
Some(source_type) => source_type, SourceType::Toml(TomlSourceType::Pyproject) => {
None => match SourceType::from(path) { let messages = if settings
SourceType::Toml(TomlSourceType::Pyproject) => { .rules
let messages = if settings .iter_enabled()
.rules .any(|rule_code| rule_code.lint_source().is_pyproject_toml())
.iter_enabled() {
.any(|rule_code| rule_code.lint_source().is_pyproject_toml()) let contents = match std::fs::read_to_string(path).map_err(SourceError::from) {
{ Ok(contents) => contents,
let contents = match std::fs::read_to_string(path).map_err(SourceError::from) { Err(err) => {
Ok(contents) => contents, return Ok(Diagnostics::from_source_error(&err, Some(path), settings));
Err(err) => { }
return Ok(Diagnostics::from_source_error(&err, Some(path), settings));
}
};
let source_file =
SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
lint_pyproject_toml(source_file, settings)
} else {
vec![]
}; };
return Ok(Diagnostics { let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
messages, lint_pyproject_toml(source_file, settings)
..Diagnostics::default() } else {
}); vec![]
} };
SourceType::Toml(_) => return Ok(Diagnostics::default()), return Ok(Diagnostics {
SourceType::Python(source_type) => source_type, messages,
}, ..Diagnostics::default()
});
}
SourceType::Toml(_) => return Ok(Diagnostics::default()),
SourceType::Python(source_type) => source_type,
}; };
// Extract the sources from the file. // Extract the sources from the file.
@@ -270,7 +263,6 @@ pub(crate) fn lint_path(
data: (messages, imports), data: (messages, imports),
error: parse_error, error: parse_error,
}, },
transformed,
fixed, fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { ) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult { if let Ok(FixerResult {
@@ -290,49 +282,26 @@ pub(crate) fn lint_path(
match fix_mode { match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?, flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
flags::FixMode::Diff => { flags::FixMode::Diff => {
write!( source_kind.diff(
transformed.as_ref(),
Some(path),
&mut io::stdout().lock(), &mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, Some(path)).unwrap()
)?; )?;
} }
flags::FixMode::Generate => {} flags::FixMode::Generate => {}
} }
} }
let transformed = if let Cow::Owned(transformed) = transformed { (result, fixed)
transformed
} else {
source_kind
};
(result, transformed, fixed)
} else { } else {
// If we fail to fix, lint the original source code. // If we fail to fix, lint the original source code.
let result = lint_only( let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, transformed, fixed) (result, fixed)
} }
} else { } else {
let result = lint_only( let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, transformed, fixed) (result, fixed)
}; };
let imports = imports.unwrap_or_default(); let imports = imports.unwrap_or_default();
@@ -340,7 +309,7 @@ pub(crate) fn lint_path(
if let Some((cache, relative_path, key)) = caching { if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors. // We don't cache parsing errors.
if parse_error.is_none() { if parse_error.is_none() {
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk, // `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
// and writing the diff to stdout, respectively). If a file has diagnostics, we // and writing the diff to stdout, respectively). If a file has diagnostics, we
// need to avoid reading from and writing to the cache in these modes. // need to avoid reading from and writing to the cache in these modes.
if match fix_mode { if match fix_mode {
@@ -355,21 +324,28 @@ pub(crate) fn lint_path(
LintCacheData::from_messages( LintCacheData::from_messages(
&messages, &messages,
imports.clone(), imports.clone(),
transformed.as_ipy_notebook().map(Notebook::index).cloned(), source_kind.as_ipy_notebook().map(Notebook::index).cloned(),
), ),
); );
} }
} }
} }
if let Some(error) = parse_error { if let Some(err) = parse_error {
error!( error!(
"{}", "{}",
DisplayParseError::from_source_kind(error, Some(path.to_path_buf()), &transformed) DisplayParseError::new(
err,
SourceCode::new(
source_kind.source_code(),
&LineIndex::from_source_text(source_kind.source_code())
),
&source_kind,
)
); );
} }
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed { let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())]) FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook.into_index())])
} else { } else {
FxHashMap::default() FxHashMap::default()
@@ -394,14 +370,8 @@ pub(crate) fn lint_stdin(
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
// TODO(charlie): Support `pyproject.toml`. // TODO(charlie): Support `pyproject.toml`.
let source_type = match path.and_then(|path| settings.linter.extension.get(path)) { let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
None => match path.map(SourceType::from).unwrap_or_default() { return Ok(Diagnostics::default());
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
return Ok(Diagnostics::default());
}
},
Some(language) => PySourceType::from(language),
}; };
// Extract the sources from the file. // Extract the sources from the file.
@@ -419,7 +389,6 @@ pub(crate) fn lint_stdin(
data: (messages, imports), data: (messages, imports),
error: parse_error, error: parse_error,
}, },
transformed,
fixed, fixed,
) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) { ) = if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {
if let Ok(FixerResult { if let Ok(FixerResult {
@@ -443,21 +412,13 @@ pub(crate) fn lint_stdin(
flags::FixMode::Diff => { flags::FixMode::Diff => {
// But only write a diff if it's non-empty. // But only write a diff if it's non-empty.
if !fixed.is_empty() { if !fixed.is_empty() {
write!( source_kind.diff(transformed.as_ref(), path, &mut io::stdout().lock())?;
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
} }
} }
flags::FixMode::Generate => {} flags::FixMode::Generate => {}
} }
let transformed = if let Cow::Owned(transformed) = transformed {
transformed (result, fixed)
} else {
source_kind
};
(result, transformed, fixed)
} else { } else {
// If we fail to fix, lint the original source code. // If we fail to fix, lint the original source code.
let result = lint_only( let result = lint_only(
@@ -467,17 +428,15 @@ pub(crate) fn lint_stdin(
noqa, noqa,
&source_kind, &source_kind,
source_type, source_type,
ParseSource::None,
); );
let fixed = FxHashMap::default();
// Write the contents to stdout anyway. // Write the contents to stdout anyway.
if fix_mode.is_apply() { if fix_mode.is_apply() {
source_kind.write(&mut io::stdout().lock())?; source_kind.write(&mut io::stdout().lock())?;
} }
let transformed = source_kind; (result, fixed)
let fixed = FxHashMap::default();
(result, transformed, fixed)
} }
} else { } else {
let result = lint_only( let result = lint_only(
@@ -487,23 +446,21 @@ pub(crate) fn lint_stdin(
noqa, noqa,
&source_kind, &source_kind,
source_type, source_type,
ParseSource::None,
); );
let transformed = source_kind;
let fixed = FxHashMap::default(); let fixed = FxHashMap::default();
(result, transformed, fixed) (result, fixed)
}; };
let imports = imports.unwrap_or_default(); let imports = imports.unwrap_or_default();
if let Some(error) = parse_error { if let Some(err) = parse_error {
error!( error!(
"{}", "Failed to parse {}: {err}",
DisplayParseError::from_source_kind(error, path.map(Path::to_path_buf), &transformed) path.map_or_else(|| "-".into(), fs::relativize_path).bold()
); );
} }
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed { let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
FxHashMap::from_iter([( FxHashMap::from_iter([(
path.map_or_else(|| "-".into(), |path| path.to_string_lossy().to_string()), path.map_or_else(|| "-".into(), |path| path.to_string_lossy().to_string()),
notebook.into_index(), notebook.into_index(),

View File

@@ -7,7 +7,6 @@ use std::process::ExitCode;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use anyhow::Result; use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory; use clap::CommandFactory;
use colored::Colorize; use colored::Colorize;
use log::warn; use log::warn;
@@ -19,7 +18,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once}; use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::args::{Args, CheckCommand, Command, FormatCommand}; use crate::args::{Args, CheckCommand, Command, FormatCommand, HelpFormat};
use crate::printer::{Flags as PrinterFlags, Printer}; use crate::printer::{Flags as PrinterFlags, Printer};
pub mod args; pub mod args;
@@ -102,25 +101,20 @@ fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
file == Path::new("-") file == Path::new("-")
} }
/// Returns the default set of files if none are provided, otherwise returns `None`. /// Get the actual value of the `format` desired from either `output_format`
fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> { /// or `format`, and warn the user if they're using the deprecated form.
if files.is_empty() { fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if is_stdin { if format.is_some() {
vec![Path::new("-").to_path_buf()] warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
} else {
vec![Path::new(".").to_path_buf()]
}
} else {
files
} }
format.unwrap_or(output_format)
} }
pub fn run( pub fn run(
Args { Args {
command, command,
global_options, log_level_args,
}: Args, }: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
{ {
let default_panic_hook = std::panic::take_hook(); let default_panic_hook = std::panic::take_hook();
@@ -148,11 +142,8 @@ pub fn run(
#[cfg(windows)] #[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok()); assert!(colored::control::set_virtual_terminal(true).is_ok());
set_up_logging(global_options.log_level())?; let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
match command { match command {
Command::Version { output_format } => { Command::Version { output_format } => {
@@ -162,8 +153,10 @@ pub fn run(
Command::Rule { Command::Rule {
rule, rule,
all, all,
output_format, format,
mut output_format,
} => { } => {
output_format = resolve_help_output_format(output_format, format);
if all { if all {
commands::rule::rules(output_format)?; commands::rule::rules(output_format)?;
} }
@@ -176,46 +169,48 @@ pub fn run(
commands::config::config(option.as_deref())?; commands::config::config(option.as_deref())?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Linter { output_format } => { Command::Linter {
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
commands::linter::linter(output_format)?; commands::linter::linter(output_format)?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Clean => { Command::Clean => {
commands::clean::clean(global_options.log_level())?; commands::clean::clean(log_level)?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::GenerateShellCompletion { shell } => { Command::GenerateShellCompletion { shell } => {
shell.generate(&mut Args::command(), &mut stdout()); shell.generate(&mut Args::command(), &mut stdout());
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Command::Check(args) => check(args, global_options), Command::Check(args) => check(args, log_level),
Command::Format(args) => format(args, global_options), Command::Format(args) => format(args, log_level),
Command::Server(args) => server(args, global_options.log_level()),
} }
} }
fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> { fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?; let (cli, overrides) = args.partition();
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
commands::format_stdin::format_stdin(&cli, &config_arguments) commands::format_stdin::format_stdin(&cli, &overrides)
} else { } else {
commands::format::format(cli, &config_arguments) commands::format::format(&cli, &overrides, log_level)
} }
} }
#[allow(clippy::needless_pass_by_value)] // TODO: remove once we start taking arguments from here pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> { let (cli, overrides) = args.partition();
let ServerCommand { preview } = args;
commands::server::run_server(preview, log_level)
}
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
// Construct the "default" settings. These are used when no `pyproject.toml` // Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy. // files are present, or files are injected from outside of the hierarchy.
let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; let pyproject_config = resolve::resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
let mut writer: Box<dyn Write> = match cli.output_file { let mut writer: Box<dyn Write> = match cli.output_file {
Some(path) if !cli.watch => { Some(path) if !cli.watch => {
@@ -227,25 +222,17 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
}; };
let stderr_writer = Box::new(BufWriter::new(io::stderr())); let stderr_writer = Box::new(BufWriter::new(io::stderr()));
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
let files = resolve_default_files(cli.files, is_stdin);
if cli.show_settings { if cli.show_settings {
commands::show_settings::show_settings( commands::show_settings::show_settings(
&files, &cli.files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
&mut writer, &mut writer,
)?; )?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
if cli.show_files { if cli.show_files {
commands::show_files::show_files( commands::show_files::show_files(&cli.files, &pyproject_config, &overrides, &mut writer)?;
&files,
&pyproject_config,
&config_arguments,
&mut writer,
)?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
@@ -257,6 +244,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
unsafe_fixes, unsafe_fixes,
output_format, output_format,
show_fixes, show_fixes,
show_source,
.. ..
} = pyproject_config.settings; } = pyproject_config.settings;
@@ -285,6 +273,9 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
if show_fixes { if show_fixes {
printer_flags |= PrinterFlags::SHOW_FIX_SUMMARY; printer_flags |= PrinterFlags::SHOW_FIX_SUMMARY;
} }
if show_source {
printer_flags |= PrinterFlags::SHOW_SOURCE;
}
if cli.ecosystem_ci { if cli.ecosystem_ci {
warn_user!( warn_user!(
"The formatting of fixes emitted by this option is a work-in-progress, subject to \ "The formatting of fixes emitted by this option is a work-in-progress, subject to \
@@ -305,8 +296,8 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
warn_user!("--fix is incompatible with --add-noqa."); warn_user!("--fix is incompatible with --add-noqa.");
} }
let modifications = let modifications =
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?; commands::add_noqa::add_noqa(&cli.files, &pyproject_config, &overrides)?;
if modifications > 0 && config_arguments.log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" }; let s = if modifications == 1 { "" } else { "s" };
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
{ {
@@ -318,30 +309,21 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let printer = Printer::new( let printer = Printer::new(
output_format, output_format,
config_arguments.log_level, log_level,
fix_mode, fix_mode,
unsafe_fixes, unsafe_fixes,
printer_flags, printer_flags,
); );
// the settings should already be combined with the CLI overrides at this point
// TODO(jane): let's make this `PreviewMode`
// TODO: this should reference the global preview mode once https://github.com/astral-sh/ruff/issues/8232
// is resolved.
let preview = pyproject_config.settings.linter.preview.is_enabled();
if cli.watch { if cli.watch {
if output_format != SerializationFormat::default(preview) { if output_format != SerializationFormat::Text {
warn_user!( warn_user!("`--output-format text` is always used in watch mode.");
"`--output-format {}` is always used in watch mode.",
SerializationFormat::default(preview)
);
} }
// Configure the file watcher. // Configure the file watcher.
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = recommended_watcher(tx)?; let mut watcher = recommended_watcher(tx)?;
for file in &files { for file in &cli.files {
watcher.watch(file, RecursiveMode::Recursive)?; watcher.watch(file, RecursiveMode::Recursive)?;
} }
if let Some(file) = pyproject_config.path.as_ref() { if let Some(file) = pyproject_config.path.as_ref() {
@@ -353,15 +335,15 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
printer.write_to_user("Starting linter in watch mode...\n"); printer.write_to_user("Starting linter in watch mode...\n");
let messages = commands::check::check( let messages = commands::check::check(
&files, &cli.files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,
unsafe_fixes, unsafe_fixes,
)?; )?;
printer.write_continuously(&mut writer, &messages, preview)?; printer.write_continuously(&mut writer, &messages)?;
// In watch mode, we may need to re-resolve the configuration. // In watch mode, we may need to re-resolve the configuration.
// TODO(charlie): Re-compute other derivative values, like the `printer`. // TODO(charlie): Re-compute other derivative values, like the `printer`.
@@ -375,41 +357,47 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
}; };
if matches!(change_kind, ChangeKind::Configuration) { if matches!(change_kind, ChangeKind::Configuration) {
pyproject_config = pyproject_config = resolve::resolve(
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),
)?;
} }
Printer::clear_screen()?; Printer::clear_screen()?;
printer.write_to_user("File change detected...\n"); printer.write_to_user("File change detected...\n");
let messages = commands::check::check( let messages = commands::check::check(
&files, &cli.files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,
unsafe_fixes, unsafe_fixes,
)?; )?;
printer.write_continuously(&mut writer, &messages, preview)?; printer.write_continuously(&mut writer, &messages)?;
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }
} }
} else { } else {
let is_stdin = is_stdin(&cli.files, cli.stdin_filename.as_deref());
// Generate lint violations. // Generate lint violations.
let diagnostics = if is_stdin { let diagnostics = if is_stdin {
commands::check_stdin::check_stdin( commands::check_stdin::check_stdin(
cli.stdin_filename.map(fs::normalize_path).as_deref(), cli.stdin_filename.map(fs::normalize_path).as_deref(),
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
noqa.into(), noqa.into(),
fix_mode, fix_mode,
)? )?
} else { } else {
commands::check::check( commands::check::check(
&files, &cli.files,
&pyproject_config, &pyproject_config,
&config_arguments, &overrides,
cache.into(), cache.into(),
noqa.into(), noqa.into(),
fix_mode, fix_mode,

View File

@@ -13,7 +13,7 @@ use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel; use ruff_linter::logging::LogLevel;
use ruff_linter::message::{ use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, SarifEmitter, TextEmitter, JsonEmitter, JsonLinesEmitter, JunitEmitter, PylintEmitter, TextEmitter,
}; };
use ruff_linter::notify_user; use ruff_linter::notify_user;
use ruff_linter::registry::{AsRule, Rule}; use ruff_linter::registry::{AsRule, Rule};
@@ -27,6 +27,8 @@ bitflags! {
pub(crate) struct Flags: u8 { pub(crate) struct Flags: u8 {
/// Whether to show violations when emitting diagnostics. /// Whether to show violations when emitting diagnostics.
const SHOW_VIOLATIONS = 0b0000_0001; const SHOW_VIOLATIONS = 0b0000_0001;
/// Whether to show the source code when emitting diagnostics.
const SHOW_SOURCE = 0b000_0010;
/// Whether to show a summary of the fixed violations when emitting diagnostics. /// Whether to show a summary of the fixed violations when emitting diagnostics.
const SHOW_FIX_SUMMARY = 0b0000_0100; const SHOW_FIX_SUMMARY = 0b0000_0100;
/// Whether to show a diff of each fixed violation when emitting diagnostics. /// Whether to show a diff of each fixed violation when emitting diagnostics.
@@ -118,14 +120,20 @@ impl Printer {
} else if remaining > 0 { } else if remaining > 0 {
let s = if remaining == 1 { "" } else { "s" }; let s = if remaining == 1 { "" } else { "s" };
writeln!(writer, "Found {remaining} error{s}.")?; writeln!(writer, "Found {remaining} error{s}.")?;
} else if remaining == 0 {
writeln!(writer, "All checks passed!")?;
} }
if let Some(fixables) = fixables { if let Some(fixables) = fixables {
let fix_prefix = format!("[{}]", "*".cyan()); let fix_prefix = format!("[{}]", "*".cyan());
if self.unsafe_fixes.is_hint() { if self.unsafe_fixes.is_enabled() {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
} else {
if fixables.applicable > 0 && fixables.unapplicable_unsafe > 0 { if fixables.applicable > 0 && fixables.unapplicable_unsafe > 0 {
let es = if fixables.unapplicable_unsafe == 1 { let es = if fixables.unapplicable_unsafe == 1 {
"" ""
@@ -155,14 +163,6 @@ impl Printer {
fixables.unapplicable_unsafe fixables.unapplicable_unsafe
)?; )?;
} }
} else {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
} }
} }
} else { } else {
@@ -218,10 +218,7 @@ impl Printer {
if !self.flags.intersects(Flags::SHOW_VIOLATIONS) { if !self.flags.intersects(Flags::SHOW_VIOLATIONS) {
if matches!( if matches!(
self.format, self.format,
SerializationFormat::Text SerializationFormat::Text | SerializationFormat::Grouped
| SerializationFormat::Full
| SerializationFormat::Concise
| SerializationFormat::Grouped
) { ) {
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
if !diagnostics.fixed.is_empty() { if !diagnostics.fixed.is_empty() {
@@ -248,12 +245,11 @@ impl Printer {
SerializationFormat::Junit => { SerializationFormat::Junit => {
JunitEmitter.emit(writer, &diagnostics.messages, &context)?; JunitEmitter.emit(writer, &diagnostics.messages, &context)?;
} }
SerializationFormat::Concise SerializationFormat::Text => {
| SerializationFormat::Full => {
TextEmitter::default() TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF)) .with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_source(self.format == SerializationFormat::Full) .with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes) .with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?; .emit(writer, &diagnostics.messages, &context)?;
@@ -269,6 +265,7 @@ impl Printer {
} }
SerializationFormat::Grouped => { SerializationFormat::Grouped => {
GroupedEmitter::default() GroupedEmitter::default()
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_unsafe_fixes(self.unsafe_fixes) .with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?; .emit(writer, &diagnostics.messages, &context)?;
@@ -294,10 +291,6 @@ impl Printer {
SerializationFormat::Azure => { SerializationFormat::Azure => {
AzureEmitter.emit(writer, &diagnostics.messages, &context)?; AzureEmitter.emit(writer, &diagnostics.messages, &context)?;
} }
SerializationFormat::Sarif => {
SarifEmitter.emit(writer, &diagnostics.messages, &context)?;
}
SerializationFormat::Text => unreachable!("Text is deprecated and should have been automatically converted to the default serialization format")
} }
writer.flush()?; writer.flush()?;
@@ -346,9 +339,7 @@ impl Printer {
} }
match self.format { match self.format {
SerializationFormat::Text SerializationFormat::Text => {
| SerializationFormat::Full
| SerializationFormat::Concise => {
// Compute the maximum number of digits in the count and code, for all messages, // Compute the maximum number of digits in the count and code, for all messages,
// to enable pretty-printing. // to enable pretty-printing.
let count_width = num_digits( let count_width = num_digits(
@@ -409,7 +400,6 @@ impl Printer {
&self, &self,
writer: &mut dyn Write, writer: &mut dyn Write,
diagnostics: &Diagnostics, diagnostics: &Diagnostics,
preview: bool,
) -> Result<()> { ) -> Result<()> {
if matches!(self.log_level, LogLevel::Silent) { if matches!(self.log_level, LogLevel::Silent) {
return Ok(()); return Ok(());
@@ -437,7 +427,7 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes); let context = EmitterContext::new(&diagnostics.notebook_indexes);
TextEmitter::default() TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(preview) .with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
.with_unsafe_fixes(self.unsafe_fixes) .with_unsafe_fixes(self.unsafe_fixes)
.emit(writer, &diagnostics.messages, &context)?; .emit(writer, &diagnostics.messages, &context)?;
} }

View File

@@ -1,4 +1,4 @@
use std::path::Path; use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use log::debug; use log::debug;
@@ -11,17 +11,19 @@ use ruff_workspace::resolver::{
Relativity, Relativity,
}; };
use crate::args::ConfigArguments; use crate::args::CliOverrides;
/// Resolve the relevant settings strategy and defaults for the current /// Resolve the relevant settings strategy and defaults for the current
/// invocation. /// invocation.
pub fn resolve( pub fn resolve(
config_arguments: &ConfigArguments, isolated: bool,
config: Option<&Path>,
overrides: &CliOverrides,
stdin_filename: Option<&Path>, stdin_filename: Option<&Path>,
) -> Result<PyprojectConfig> { ) -> Result<PyprojectConfig> {
// First priority: if we're running in isolated mode, use the default settings. // First priority: if we're running in isolated mode, use the default settings.
if config_arguments.isolated { if isolated {
let config = config_arguments.transform(Configuration::default()); let config = overrides.transform(Configuration::default());
let settings = config.into_settings(&path_dedot::CWD)?; let settings = config.into_settings(&path_dedot::CWD)?;
debug!("Isolated mode, not reading any pyproject.toml"); debug!("Isolated mode, not reading any pyproject.toml");
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
@@ -34,16 +36,20 @@ pub fn resolve(
// Second priority: the user specified a `pyproject.toml` file. Use that // Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the // `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.) // current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config_arguments.config_file() { if let Some(pyproject) = config
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?; .map(|config| config.display().to_string())
.map(|config| shellexpand::full(&config).map(|config| PathBuf::from(config.as_ref())))
.transpose()?
{
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
debug!( debug!(
"Using user-specified configuration file at: {}", "Using user specified pyproject.toml at {}",
pyproject.display() pyproject.display()
); );
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed, PyprojectDiscoveryStrategy::Fixed,
settings, settings,
Some(pyproject.to_path_buf()), Some(pyproject),
)); ));
} }
@@ -57,11 +63,8 @@ pub fn resolve(
.as_ref() .as_ref()
.unwrap_or(&path_dedot::CWD.as_path()), .unwrap_or(&path_dedot::CWD.as_path()),
)? { )? {
debug!( debug!("Using pyproject.toml (parent) at {}", pyproject.display());
"Using configuration file (via parent) at: {}", let settings = resolve_root_settings(&pyproject, Relativity::Parent, overrides)?;
pyproject.display()
);
let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?;
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,
settings, settings,
@@ -74,11 +77,8 @@ pub fn resolve(
// end up the "closest" `pyproject.toml` file for every Python file later on, so // end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.) // these act as the "default" settings.)
if let Some(pyproject) = pyproject::find_user_settings_toml() { if let Some(pyproject) = pyproject::find_user_settings_toml() {
debug!( debug!("Using pyproject.toml (cwd) at {}", pyproject.display());
"Using configuration file (via cwd) at: {}", let settings = resolve_root_settings(&pyproject, Relativity::Cwd, overrides)?;
pyproject.display()
);
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
return Ok(PyprojectConfig::new( return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,
settings, settings,
@@ -91,7 +91,7 @@ pub fn resolve(
// "closest" `pyproject.toml` file for every Python file later on, so these act // "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.) // as the "default" settings.)
debug!("Using Ruff default settings"); debug!("Using Ruff default settings");
let config = config_arguments.transform(Configuration::default()); let config = overrides.transform(Configuration::default());
let settings = config.into_settings(&path_dedot::CWD)?; let settings = config.into_settings(&path_dedot::CWD)?;
Ok(PyprojectConfig::new( Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,

View File

@@ -0,0 +1,5 @@
---
source: crates/ruff_cli/src/version.rs
expression: version
---
0.0.0

View File

@@ -1,5 +1,5 @@
--- ---
source: crates/ruff/src/version.rs source: crates/ruff_cli/src/version.rs
expression: version expression: version
--- ---
0.0.0 (53b0f5d92 2023-10-19) 0.0.0 (53b0f5d92 2023-10-19)

View File

@@ -1,5 +1,5 @@
--- ---
source: crates/ruff/src/version.rs source: crates/ruff_cli/src/version.rs
expression: version expression: version
--- ---
0.0.0+24 (53b0f5d92 2023-10-19) 0.0.0+24 (53b0f5d92 2023-10-19)

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