Compare commits

..

3 Commits

Author SHA1 Message Date
Micha Reiser
dc24d01b2e Implicit string concat formatting 2024-02-14 17:54:12 +01:00
Micha Reiser
5a9d656bc4 Extract normalize into its own submodule 2024-02-14 17:22:45 +01:00
Micha Reiser
33184dc6a4 Extract AnyString nodes from string/mod 2024-02-14 17:14:28 +01:00
2323 changed files with 124403 additions and 176431 deletions

4
.gitattributes vendored
View File

@@ -2,13 +2,9 @@
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/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
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json linguist-generated=true text=auto eol=lf
*.md.snap linguist-language=Markdown

12
.github/CODEOWNERS vendored
View File

@@ -5,13 +5,5 @@
# - The '*' pattern is global owners.
# - Order is important. The last matching pattern has the most precedence.
/crates/ruff_notebook/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser @dhruvmanila
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood
# Script for fuzzing the parser
/scripts/fuzz-parser/ @AlexWaygood
# Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila

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:
* 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.
* 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`).

21
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
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"
directory: "/"
schedule:
interval: "weekly"
labels: ["internal"]

View File

@@ -1,68 +0,0 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
dependencyDashboard: true,
suppressNotifications: ["prEditedNotification"],
extends: ["config:recommended"],
labels: ["internal"],
schedule: ["before 4am on Monday"],
semanticCommits: "disabled",
separateMajorMinor: false,
prHourlyLimit: 10,
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "npm"],
cargo: {
// See https://docs.renovatebot.com/configuration-options/#rangestrategy
rangeStrategy: "update-lockfile",
},
pep621: {
fileMatch: ["^(python|scripts)/.*pyproject\\.toml$"],
},
npm: {
fileMatch: ["^playground/.*package\\.json$"],
},
"pre-commit": {
enabled: true,
},
packageRules: [
{
// Group upload/download artifact updates, the versions are dependent
groupName: "Artifact GitHub Actions dependencies",
matchManagers: ["github-actions"],
matchPackagePatterns: ["actions/.*-artifact"],
description: "Weekly update of artifact-related GitHub Actions dependencies",
},
{
groupName: "pre-commit dependencies",
matchManagers: ["pre-commit"],
description: "Weekly update of pre-commit dependencies",
},
{
groupName: "NPM Development dependencies",
matchManagers: ["npm"],
matchDepTypes: ["devDependencies"],
description: "Weekly update of NPM development dependencies",
},
{
groupName: "Monaco",
matchManagers: ["npm"],
matchPackagePatterns: ["monaco"],
description: "Weekly update of the Monaco editor",
},
{
groupName: "strum",
matchManagers: ["cargo"],
matchPackagePatterns: ["strum"],
description: "Weekly update of strum dependencies",
},
{
groupName: "ESLint",
matchManagers: ["npm"],
matchPackageNames: ["eslint"],
allowedVersions: "<9",
description: "Constraint ESLint to version 8 until TypeScript-eslint supports ESLint 9", // https://github.com/typescript-eslint/typescript-eslint/issues/8211
},
],
vulnerabilityAlerts: {
commitMessageSuffix: "",
labels: ["internal", "security"],
},
}

View File

@@ -23,8 +23,6 @@ jobs:
name: "Determine changes"
runs-on: ubuntu-latest
outputs:
# Flag that is raised when any code that affects parser is changed
parser: ${{ steps.changed.outputs.parser_any_changed }}
# Flag that is raised when any code that affects linter is changed
linter: ${{ steps.changed.outputs.linter_any_changed }}
# Flag that is raised when any code that affects formatter is changed
@@ -37,21 +35,10 @@ jobs:
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v44
- uses: tj-actions/changed-files@v42
id: changed
with:
files_yaml: |
parser:
- Cargo.toml
- Cargo.lock
- crates/ruff_python_trivia/**
- crates/ruff_source_file/**
- crates/ruff_text_size/**
- crates/ruff_python_ast/**
- crates/ruff_python_parser/**
- scripts/fuzz-parser/**
- .github/workflows/ci.yaml
linter:
- Cargo.toml
- Cargo.lock
@@ -146,7 +133,7 @@ jobs:
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
- uses: actions/upload-artifact@v3
with:
name: ruff
path: target/debug/ruff
@@ -213,38 +200,6 @@ jobs:
tool: cargo-fuzz@0.11.2
- run: cargo fuzz build -s none
fuzz-parser:
name: "Fuzz the parser"
runs-on: ubuntu-latest
needs:
- cargo-test-linux
- determine_changes
if: ${{ needs.determine_changes.outputs.parser == 'true' }}
timeout-minutes: 20
env:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install Python requirements
run: uv pip install -r scripts/fuzz-parser/requirements.txt --system
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
id: download-cached-binary
with:
name: ruff
path: ruff-to-test
- name: Fuzz
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
python scripts/fuzz-parser/fuzz.py 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
scripts:
name: "test scripts"
runs-on: ubuntu-latest
@@ -273,7 +228,9 @@ jobs:
- determine_changes
# 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' && needs.determine_changes.outputs.code == 'true' }}
if: github.event_name == 'pull_request' && ${{
needs.determine_changes.outputs.code == 'true'
}}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -281,7 +238,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -293,7 +250,6 @@ jobs:
with:
name: ruff
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- name: Install ruff-ecosystem
@@ -368,13 +324,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
name: Upload Results
with:
name: ecosystem-result
@@ -515,7 +471,7 @@ jobs:
- determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: extractions/setup-just@v2
- uses: extractions/setup-just@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -528,7 +484,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
name: Download development ruff binary
id: ruff-target
with:
@@ -552,7 +508,7 @@ jobs:
benchmarks:
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -568,23 +524,8 @@ jobs:
- uses: Swatinem/rust-cache@v2
# Codspeed comes with a very ancient cargo version (1.66) that resolves features flags differently than what we use now.
# This can result in build failures; see https://github.com/astral-sh/ruff/pull/10700.
# There's a pending codspeed PR to upgrade to a newer cargo version, but until that's merged, we need to use the workaround below.
# https://github.com/CodSpeedHQ/codspeed-rust/pull/31
# What we do is to call cargo build manually with the correct feature flags and RUSTC settings. We'll have to
# manually maintain the list of benchmarks to run with codspeed (the benefit is that we could detect which benchmarks to run and build based on the changes).
# This is inspired by https://github.com/oxc-project/oxc/blob/a0532adc654039a0c7ead7b35216dfa0b0cb8e8f/.github/workflows/benchmark.yml
- name: "Build benchmarks"
env:
RUSTFLAGS: "-C debuginfo=2 -C strip=none -g --cfg codspeed"
shell: bash
# Build all benchmarks, copy the binary to the codspeed directory, remove any `*.d` files that might have been created.
run: |
cargo build --release -p ruff_benchmark --bench parser --bench linter --bench formatter --bench lexer --features=codspeed
mkdir -p ./target/codspeed/ruff_benchmark
cp ./target/release/deps/{lexer,parser,linter,formatter}* target/codspeed/ruff_benchmark/
rm -rf ./target/codspeed/ruff_benchmark/*.d
run: cargo codspeed build --features codspeed -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@v2

View File

@@ -52,13 +52,13 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload sdist"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-sdist
name: wheels
path: dist
macos-x86_64:
runs-on: macos-12
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
@@ -80,9 +80,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-macos-x86_64
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -90,15 +90,15 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-macos-x86_64
name: binaries
path: |
*.tar.gz
*.sha256
macos-universal:
runs-on: macos-12
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
@@ -119,9 +119,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-aarch64-apple-darwin
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -129,9 +129,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-aarch64-apple-darwin
name: binaries
path: |
*.tar.gz
*.sha256
@@ -170,9 +170,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.platform.target }}
name: wheels
path: dist
- name: "Archive binary"
shell: bash
@@ -181,9 +181,9 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.platform.target }}
name: binaries
path: |
*.zip
*.sha256
@@ -218,9 +218,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.target }}
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -228,9 +228,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.target }}
name: binaries
path: |
*.tar.gz
*.sha256
@@ -251,12 +251,8 @@ jobs:
arch: s390x
- target: powerpc64le-unknown-linux-gnu
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
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps:
- uses: actions/checkout@v4
@@ -289,9 +285,9 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.platform.target }}
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -299,9 +295,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.platform.target }}
name: binaries
path: |
*.tar.gz
*.sha256
@@ -341,9 +337,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.target }}
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -351,9 +347,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.target }}
name: binaries
path: |
*.tar.gz
*.sha256
@@ -398,9 +394,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.platform.target }}
name: wheels
path: dist
- name: "Archive binary"
run: |
@@ -408,9 +404,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.platform.target }}
name: binaries
path: |
*.tar.gz
*.sha256
@@ -467,11 +463,10 @@ jobs:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
pattern: wheels-*
name: wheels
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
@@ -511,13 +506,12 @@ jobs:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
pattern: binaries-*
name: binaries
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
with:
draft: true
files: binaries/*

1
.gitignore vendored
View File

@@ -92,7 +92,6 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
repos/
# Translations
*.mo

View File

@@ -17,4 +17,4 @@ MD013: false
# MD024/no-duplicate-heading
MD024:
# Allow when nested under different parents e.g. CHANGELOG.md
siblings_only: true
allow_different_nesting: true

View File

@@ -13,7 +13,7 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16
rev: v0.15
hooks:
- id: validate-pyproject
@@ -31,7 +31,7 @@ repos:
)$
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.39.0
rev: v0.37.0
hooks:
- id: markdownlint-fix
exclude: |
@@ -41,7 +41,7 @@ repos:
)$
- repo: https://github.com/crate-ci/typos
rev: v1.20.9
rev: v1.16.22
hooks:
- id: typos
@@ -55,7 +55,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1
rev: v0.1.4
hooks:
- id: ruff-format
- id: ruff
@@ -70,7 +70,7 @@ repos:
# Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
rev: v3.0.3
hooks:
- id: prettier
types: [yaml]

View File

@@ -1,56 +1,5 @@
# 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))

View File

@@ -1,496 +1,5 @@
# Changelog
## 0.4.2
### Rule changes
- \[`flake8-pyi`\] Allow for overloaded `__exit__` and `__aexit__` definitions (`PYI036`) ([#11057](https://github.com/astral-sh/ruff/pull/11057))
- \[`pyupgrade`\] Catch usages of `"%s" % var` and provide an unsafe fix (`UP031`) ([#11019](https://github.com/astral-sh/ruff/pull/11019))
- \[`refurb`\] Implement new rule that suggests min/max over `sorted()` (`FURB192`) ([#10868](https://github.com/astral-sh/ruff/pull/10868))
### Server
- Fix an issue with missing diagnostics for Neovim and Helix ([#11092](https://github.com/astral-sh/ruff/pull/11092))
- Implement hover documentation for `noqa` codes ([#11096](https://github.com/astral-sh/ruff/pull/11096))
- Introduce common Ruff configuration options with new server settings ([#11062](https://github.com/astral-sh/ruff/pull/11062))
### Bug fixes
- Use `macos-12` for building release wheels to enable macOS 11 compatibility ([#11146](https://github.com/astral-sh/ruff/pull/11146))
- \[`flake8-blind-expect`\] Allow raise from in `BLE001` ([#11131](https://github.com/astral-sh/ruff/pull/11131))
- \[`flake8-pyi`\] Allow simple assignments to `None` in enum class scopes (`PYI026`) ([#11128](https://github.com/astral-sh/ruff/pull/11128))
- \[`flake8-simplify`\] Avoid raising `SIM911` for non-`zip` attribute calls ([#11126](https://github.com/astral-sh/ruff/pull/11126))
- \[`refurb`\] Avoid `operator.itemgetter` suggestion for single-item tuple ([#11095](https://github.com/astral-sh/ruff/pull/11095))
- \[`ruff`\] Respect per-file-ignores for `RUF100` with no other diagnostics ([#11058](https://github.com/astral-sh/ruff/pull/11058))
- \[`ruff`\] Fix async comprehension false positive (`RUF029`) ([#11070](https://github.com/astral-sh/ruff/pull/11070))
### Documentation
- \[`flake8-bugbear`\] Document explicitly disabling strict zip (`B905`) ([#11040](https://github.com/astral-sh/ruff/pull/11040))
- \[`flake8-type-checking`\] Mention `lint.typing-modules` in `TCH001`, `TCH002`, and `TCH003` ([#11144](https://github.com/astral-sh/ruff/pull/11144))
- \[`isort`\] Improve documentation around custom `isort` sections ([#11050](https://github.com/astral-sh/ruff/pull/11050))
- \[`pylint`\] Fix documentation oversight for `invalid-X-returns` ([#11094](https://github.com/astral-sh/ruff/pull/11094))
### Performance
- Use `matchit` to resolve per-file settings ([#11111](https://github.com/astral-sh/ruff/pull/11111))
## 0.4.1
### Preview features
- \[`pylint`\] Implement `invalid-hash-returned` (`PLE0309`) ([#10961](https://github.com/astral-sh/ruff/pull/10961))
- \[`pylint`\] Implement `invalid-index-returned` (`PLE0305`) ([#10962](https://github.com/astral-sh/ruff/pull/10962))
### Bug fixes
- \[`pylint`\] Allow `NoReturn`-like functions for `__str__`, `__len__`, etc. (`PLE0307`) ([#11017](https://github.com/astral-sh/ruff/pull/11017))
- Parser: Use empty range when there's "gap" in token source ([#11032](https://github.com/astral-sh/ruff/pull/11032))
- \[`ruff`\] Ignore stub functions in `unused-async` (`RUF029`) ([#11026](https://github.com/astral-sh/ruff/pull/11026))
- Parser: Expect indented case block instead of match stmt ([#11033](https://github.com/astral-sh/ruff/pull/11033))
## 0.4.0
### A new, hand-written parser
Ruff's new parser is **>2x faster**, which translates to a **20-40% speedup** for all linting and formatting invocations.
There's a lot to say about this exciting change, so check out the [blog post](https://astral.sh/blog/ruff-v0.4.0) for more details!
See [#10036](https://github.com/astral-sh/ruff/pull/10036) for implementation details.
### A new language server in Rust
With this release, we also want to highlight our new language server. `ruff server` is a Rust-powered language
server that comes built-in with Ruff. It can be used with any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP).
It uses a multi-threaded, lock-free architecture inspired by `rust-analyzer` and it will open the door for a lot
of exciting features. Its also faster than our previous [Python-based language server](https://github.com/astral-sh/ruff-lsp)
-- but you probably guessed that already.
`ruff server` is only in alpha, but it has a lot of features that you can try out today:
- Lints Python files automatically and shows quick-fixes when available
- Formats Python files, with support for range formatting
- Comes with commands for quickly performing actions: `ruff.applyAutofix`, `ruff.applyFormat`, and `ruff.applyOrganizeImports`
- Supports `source.fixAll` and `source.organizeImports` source actions
- Automatically reloads your project configuration when you change it
To setup `ruff server` with your editor, refer to the [README.md](https://github.com/astral-sh/ruff/blob/main/crates/ruff_server/README.md).
### Preview features
- \[`pycodestyle`\] Do not trigger `E3` rules on `def`s following a function/method with a dummy body ([#10704](https://github.com/astral-sh/ruff/pull/10704))
- \[`pylint`\] Implement `invalid-bytes-returned` (`E0308`) ([#10959](https://github.com/astral-sh/ruff/pull/10959))
- \[`pylint`\] Implement `invalid-length-returned` (`E0303`) ([#10963](https://github.com/astral-sh/ruff/pull/10963))
- \[`pylint`\] Implement `self-cls-assignment` (`W0642`) ([#9267](https://github.com/astral-sh/ruff/pull/9267))
- \[`pylint`\] Omit stubs from `invalid-bool` and `invalid-str-return-type` ([#11008](https://github.com/astral-sh/ruff/pull/11008))
- \[`ruff`\] New rule `unused-async` (`RUF029`) to detect unneeded `async` keywords on functions ([#9966](https://github.com/astral-sh/ruff/pull/9966))
### Rule changes
- \[`flake8-bandit`\] Allow `urllib.request.urlopen` calls with static `Request` argument (`S310`) ([#10964](https://github.com/astral-sh/ruff/pull/10964))
- \[`flake8-bugbear`\] Treat `raise NotImplemented`-only bodies as stub functions (`B006`) ([#10990](https://github.com/astral-sh/ruff/pull/10990))
- \[`flake8-slots`\] Respect same-file `Enum` subclasses (`SLOT000`) ([#11006](https://github.com/astral-sh/ruff/pull/11006))
- \[`pylint`\] Support inverted comparisons (`PLR1730`) ([#10920](https://github.com/astral-sh/ruff/pull/10920))
### Linter
- Improve handling of builtin symbols in linter rules ([#10919](https://github.com/astral-sh/ruff/pull/10919))
- Improve display of rules in `--show-settings` ([#11003](https://github.com/astral-sh/ruff/pull/11003))
- Improve inference capabilities of the `BuiltinTypeChecker` ([#10976](https://github.com/astral-sh/ruff/pull/10976))
- Resolve classes and functions relative to script name ([#10965](https://github.com/astral-sh/ruff/pull/10965))
- Improve performance of `RuleTable::any_enabled` ([#10971](https://github.com/astral-sh/ruff/pull/10971))
### Server
*This section is devoted to updates for our new language server, written in Rust.*
- Enable ruff-specific source actions ([#10916](https://github.com/astral-sh/ruff/pull/10916))
- Refreshes diagnostics for open files when file configuration is changed ([#10988](https://github.com/astral-sh/ruff/pull/10988))
- Important errors are now shown as popups ([#10951](https://github.com/astral-sh/ruff/pull/10951))
- Introduce settings for directly configuring the linter and formatter ([#10984](https://github.com/astral-sh/ruff/pull/10984))
- Resolve configuration for each document individually ([#10950](https://github.com/astral-sh/ruff/pull/10950))
- Write a setup guide for Neovim ([#10987](https://github.com/astral-sh/ruff/pull/10987))
### Configuration
- Add `RUFF_OUTPUT_FILE` environment variable support ([#10992](https://github.com/astral-sh/ruff/pull/10992))
### Bug fixes
- Avoid `non-augmented-assignment` for reversed, non-commutative operators (`PLR6104`) ([#10909](https://github.com/astral-sh/ruff/pull/10909))
- Limit commutative non-augmented-assignments to primitive data types (`PLR6104`) ([#10912](https://github.com/astral-sh/ruff/pull/10912))
- Respect `per-file-ignores` for `RUF100` on blanket `# noqa` ([#10908](https://github.com/astral-sh/ruff/pull/10908))
- Consider `if` expression for parenthesized with items parsing ([#11010](https://github.com/astral-sh/ruff/pull/11010))
- Consider binary expr for parenthesized with items parsing ([#11012](https://github.com/astral-sh/ruff/pull/11012))
- Reset `FOR_TARGET` context for all kinds of parentheses ([#11009](https://github.com/astral-sh/ruff/pull/11009))
## 0.3.7
### Preview features
- \[`flake8-bugbear`\] Implement `loop-iterator-mutation` (`B909`) ([#9578](https://github.com/astral-sh/ruff/pull/9578))
- \[`pylint`\] Implement rule to prefer augmented assignment (`PLR6104`) ([#9932](https://github.com/astral-sh/ruff/pull/9932))
### Bug fixes
- Avoid TOCTOU errors in cache initialization ([#10884](https://github.com/astral-sh/ruff/pull/10884))
- \[`pylint`\] Recode `nan-comparison` rule to `W0177` ([#10894](https://github.com/astral-sh/ruff/pull/10894))
- \[`pylint`\] Reverse min-max logic in `if-stmt-min-max` ([#10890](https://github.com/astral-sh/ruff/pull/10890))
## 0.3.6
### Preview features
- \[`pylint`\] Implement `bad-staticmethod-argument` (`PLW0211`) ([#10781](https://github.com/astral-sh/ruff/pull/10781))
- \[`pylint`\] Implement `if-stmt-min-max` (`PLR1730`, `PLR1731`) ([#10002](https://github.com/astral-sh/ruff/pull/10002))
- \[`pyupgrade`\] Replace `str,Enum` multiple inheritance with `StrEnum` `UP042` ([#10713](https://github.com/astral-sh/ruff/pull/10713))
- \[`refurb`\] Implement `if-expr-instead-of-or-operator` (`FURB110`) ([#10687](https://github.com/astral-sh/ruff/pull/10687))
- \[`refurb`\] Implement `int-on-sliced-str` (`FURB166`) ([#10650](https://github.com/astral-sh/ruff/pull/10650))
- \[`refurb`\] Implement `write-whole-file` (`FURB103`) ([#10802](https://github.com/astral-sh/ruff/pull/10802))
- \[`refurb`\] Support `itemgetter` in `reimplemented-operator` (`FURB118`) ([#10526](https://github.com/astral-sh/ruff/pull/10526))
- \[`flake8_comprehensions`\] Add `sum`/`min`/`max` to unnecessary comprehension check (`C419`) ([#10759](https://github.com/astral-sh/ruff/pull/10759))
### Rule changes
- \[`pydocstyle`\] Require capitalizing docstrings where the first sentence is a single word (`D403`) ([#10776](https://github.com/astral-sh/ruff/pull/10776))
- \[`pycodestyle`\] Ignore annotated lambdas in class scopes (`E731`) ([#10720](https://github.com/astral-sh/ruff/pull/10720))
- \[`flake8-pyi`\] Various improvements to PYI034 ([#10807](https://github.com/astral-sh/ruff/pull/10807))
- \[`flake8-slots`\] Flag subclasses of call-based `typing.NamedTuple`s as well as subclasses of `collections.namedtuple()` (`SLOT002`) ([#10808](https://github.com/astral-sh/ruff/pull/10808))
- \[`pyflakes`\] Allow forward references in class bases in stub files (`F821`) ([#10779](https://github.com/astral-sh/ruff/pull/10779))
- \[`pygrep-hooks`\] Improve `blanket-noqa` error message (`PGH004`) ([#10851](https://github.com/astral-sh/ruff/pull/10851))
### CLI
- Support `FORCE_COLOR` env var ([#10839](https://github.com/astral-sh/ruff/pull/10839))
### Configuration
- Support negated patterns in `[extend-]per-file-ignores` ([#10852](https://github.com/astral-sh/ruff/pull/10852))
### Bug fixes
- \[`flake8-import-conventions`\] Accept non-aliased (but correct) import in `unconventional-import-alias` (`ICN001`) ([#10729](https://github.com/astral-sh/ruff/pull/10729))
- \[`flake8-quotes`\] Add semantic model flag when inside f-string replacement field ([#10766](https://github.com/astral-sh/ruff/pull/10766))
- \[`pep8-naming`\] Recursively resolve `TypeDicts` for N815 violations ([#10719](https://github.com/astral-sh/ruff/pull/10719))
- \[`flake8-quotes`\] Respect `Q00*` ignores in `flake8-quotes` rules ([#10728](https://github.com/astral-sh/ruff/pull/10728))
- \[`flake8-simplify`\] Show negated condition in `needless-bool` diagnostics (`SIM103`) ([#10854](https://github.com/astral-sh/ruff/pull/10854))
- \[`ruff`\] Use within-scope shadowed bindings in `asyncio-dangling-task` (`RUF006`) ([#10793](https://github.com/astral-sh/ruff/pull/10793))
- \[`flake8-pytest-style`\] Fix single-tuple conversion in `pytest-parametrize-values-wrong-type` (`PT007`) ([#10862](https://github.com/astral-sh/ruff/pull/10862))
- \[`flake8-return`\] Ignore assignments to annotated variables in `unnecessary-assign` (`RET504`) ([#10741](https://github.com/astral-sh/ruff/pull/10741))
- \[`refurb`\] Do not allow any keyword arguments for `read-whole-file` in `rb` mode (`FURB101`) ([#10803](https://github.com/astral-sh/ruff/pull/10803))
- \[`pylint`\] Don't recommend decorating staticmethods with `@singledispatch` (`PLE1519`, `PLE1520`) ([#10637](https://github.com/astral-sh/ruff/pull/10637))
- \[`pydocstyle`\] Use section name range for all section-related docstring diagnostics ([#10740](https://github.com/astral-sh/ruff/pull/10740))
- Respect `# noqa` directives on `__all__` openers ([#10798](https://github.com/astral-sh/ruff/pull/10798))
## 0.3.5
### Preview features
- \[`pylint`\] Implement `modified-iterating-set` (`E4703`) ([#10473](https://github.com/astral-sh/ruff/pull/10473))
- \[`refurb`\] Implement `for-loop-set-mutations` (`FURB142`) ([#10583](https://github.com/astral-sh/ruff/pull/10583))
- \[`refurb`\] Implement `unnecessary-from-float` (`FURB164`) ([#10647](https://github.com/astral-sh/ruff/pull/10647))
- \[`refurb`\] Implement `verbose-decimal-constructor` (`FURB157`) ([#10533](https://github.com/astral-sh/ruff/pull/10533))
### Rule changes
- \[`flake8-comprehensions`\] Handled special case for `C401` which also matches `C416` ([#10596](https://github.com/astral-sh/ruff/pull/10596))
- \[`flake8-pyi`\] Mark `unaliased-collections-abc-set-import` fix as "safe" for more cases in stub files (`PYI025`) ([#10547](https://github.com/astral-sh/ruff/pull/10547))
- \[`numpy`\] Add `row_stack` to NumPy 2.0 migration rule ([#10646](https://github.com/astral-sh/ruff/pull/10646))
- \[`pycodestyle`\] Allow cell magics before an import (`E402`) ([#10545](https://github.com/astral-sh/ruff/pull/10545))
- \[`pycodestyle`\] Avoid blank line rules for the first logical line in cell ([#10291](https://github.com/astral-sh/ruff/pull/10291))
### Configuration
- Respected nested namespace packages ([#10541](https://github.com/astral-sh/ruff/pull/10541))
- \[`flake8-boolean-trap`\] Add setting for user defined allowed boolean trap ([#10531](https://github.com/astral-sh/ruff/pull/10531))
### Bug fixes
- Correctly handle references in `__all__` definitions when renaming symbols in autofixes ([#10527](https://github.com/astral-sh/ruff/pull/10527))
- Track ranges of names inside `__all__` definitions ([#10525](https://github.com/astral-sh/ruff/pull/10525))
- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`) ([#10539](https://github.com/astral-sh/ruff/pull/10539))
- \[`flake8-copyright`\] Accept commas in default copyright pattern ([#9498](https://github.com/astral-sh/ruff/pull/9498))
- \[`flake8-datetimez`\] Allow f-strings with `%z` for `DTZ007` ([#10651](https://github.com/astral-sh/ruff/pull/10651))
- \[`flake8-pytest-style`\] Fix `PT014` autofix for last item in list ([#10532](https://github.com/astral-sh/ruff/pull/10532))
- \[`flake8-quotes`\] Ignore `Q000`, `Q001` when string is inside forward ref ([#10585](https://github.com/astral-sh/ruff/pull/10585))
- \[`isort`\] Always place non-relative imports after relative imports ([#10669](https://github.com/astral-sh/ruff/pull/10669))
- \[`isort`\] Respect Unicode characters in import sorting ([#10529](https://github.com/astral-sh/ruff/pull/10529))
- \[`pyflakes`\] Fix F821 false negatives when `from __future__ import annotations` is active (attempt 2) ([#10524](https://github.com/astral-sh/ruff/pull/10524))
- \[`pyflakes`\] Make `unnecessary-lambda` an always-unsafe fix ([#10668](https://github.com/astral-sh/ruff/pull/10668))
- \[`pylint`\] Fixed false-positive on the rule `PLW1641` (`eq-without-hash`) ([#10566](https://github.com/astral-sh/ruff/pull/10566))
- \[`ruff`\] Fix panic in unused `# noqa` removal with multi-byte space (`RUF100`) ([#10682](https://github.com/astral-sh/ruff/pull/10682))
### Documentation
- Add PR title format to `CONTRIBUTING.md` ([#10665](https://github.com/astral-sh/ruff/pull/10665))
- Fix list markup to include blank lines required ([#10591](https://github.com/astral-sh/ruff/pull/10591))
- Put `flake8-logging` next to the other flake8 plugins in registry ([#10587](https://github.com/astral-sh/ruff/pull/10587))
- \[`flake8-bandit`\] Update warning message for rule `S305` to address insecure block cipher mode use ([#10602](https://github.com/astral-sh/ruff/pull/10602))
- \[`flake8-bugbear`\] Document use of anonymous assignment in `useless-expression` ([#10551](https://github.com/astral-sh/ruff/pull/10551))
- \[`flake8-datetimez`\] Clarify error messages and docs for `DTZ` rules ([#10621](https://github.com/astral-sh/ruff/pull/10621))
- \[`pycodestyle`\] Use same before vs. after numbers for `space-around-operator` ([#10640](https://github.com/astral-sh/ruff/pull/10640))
- \[`ruff`\] Change `quadratic-list-summation` docs to use `iadd` consistently ([#10666](https://github.com/astral-sh/ruff/pull/10666))
## 0.3.4
### Preview features
- \[`flake8-simplify`\] Detect implicit `else` cases in `needless-bool` (`SIM103`) ([#10414](https://github.com/astral-sh/ruff/pull/10414))
- \[`pylint`\] Implement `nan-comparison` (`PLW0117`) ([#10401](https://github.com/astral-sh/ruff/pull/10401))
- \[`pylint`\] Implement `nonlocal-and-global` (`E115`) ([#10407](https://github.com/astral-sh/ruff/pull/10407))
- \[`pylint`\] Implement `singledispatchmethod-function` (`PLE5120`) ([#10428](https://github.com/astral-sh/ruff/pull/10428))
- \[`refurb`\] Implement `list-reverse-copy` (`FURB187`) ([#10212](https://github.com/astral-sh/ruff/pull/10212))
### Rule changes
- \[`flake8-pytest-style`\] Add automatic fix for `pytest-parametrize-values-wrong-type` (`PT007`) ([#10461](https://github.com/astral-sh/ruff/pull/10461))
- \[`pycodestyle`\] Allow SPDX license headers to exceed the line length (`E501`) ([#10481](https://github.com/astral-sh/ruff/pull/10481))
### Formatter
- Fix unstable formatting for trailing subscript end-of-line comment ([#10492](https://github.com/astral-sh/ruff/pull/10492))
### Bug fixes
- Avoid code comment detection in PEP 723 script tags ([#10464](https://github.com/astral-sh/ruff/pull/10464))
- Avoid incorrect tuple transformation in single-element case (`C409`) ([#10491](https://github.com/astral-sh/ruff/pull/10491))
- Bug fix: Prevent fully defined links [`name`](link) from being reformatted ([#10442](https://github.com/astral-sh/ruff/pull/10442))
- Consider raw source code for `W605` ([#10480](https://github.com/astral-sh/ruff/pull/10480))
- Docs: Link inline settings when not part of options section ([#10499](https://github.com/astral-sh/ruff/pull/10499))
- Don't treat annotations as redefinitions in `.pyi` files ([#10512](https://github.com/astral-sh/ruff/pull/10512))
- Fix `E231` bug: Inconsistent catch compared to pycodestyle, such as when dict nested in list ([#10469](https://github.com/astral-sh/ruff/pull/10469))
- Fix pylint upstream categories not showing in docs ([#10441](https://github.com/astral-sh/ruff/pull/10441))
- Add missing `Options` references to blank line docs ([#10498](https://github.com/astral-sh/ruff/pull/10498))
- 'Revert "F821: Fix false negatives in .py files when `from __future__ import annotations` is active (#10362)"' ([#10513](https://github.com/astral-sh/ruff/pull/10513))
- Apply NFKC normalization to unicode identifiers in the lexer ([#10412](https://github.com/astral-sh/ruff/pull/10412))
- Avoid failures due to non-deterministic binding ordering ([#10478](https://github.com/astral-sh/ruff/pull/10478))
- \[`flake8-bugbear`\] Allow tuples of exceptions (`B030`) ([#10437](https://github.com/astral-sh/ruff/pull/10437))
- \[`flake8-quotes`\] Avoid syntax errors due to invalid quotes (`Q000, Q002`) ([#10199](https://github.com/astral-sh/ruff/pull/10199))
## 0.3.3
### Preview features
- \[`flake8-bandit`\]: Implement `S610` rule ([#10316](https://github.com/astral-sh/ruff/pull/10316))
- \[`pycodestyle`\] Implement `blank-line-at-end-of-file` (`W391`) ([#10243](https://github.com/astral-sh/ruff/pull/10243))
- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`) ([#10292](https://github.com/astral-sh/ruff/pull/10292))
- \[`pylint`\] - implement `redeclared-assigned-name` (`W0128`) ([#9268](https://github.com/astral-sh/ruff/pull/9268))
### Rule changes
- \[`flake8_comprehensions`\] Handled special case for `C400` which also matches `C416` ([#10419](https://github.com/astral-sh/ruff/pull/10419))
- \[`flake8-bandit`\] Implement upstream updates for `S311`, `S324` and `S605` ([#10313](https://github.com/astral-sh/ruff/pull/10313))
- \[`pyflakes`\] Remove `F401` fix for `__init__` imports by default and allow opt-in to unsafe fix ([#10365](https://github.com/astral-sh/ruff/pull/10365))
- \[`pylint`\] Implement `invalid-bool-return-type` (`E304`) ([#10377](https://github.com/astral-sh/ruff/pull/10377))
- \[`pylint`\] Include builtin warnings in useless-exception-statement (`PLW0133`) ([#10394](https://github.com/astral-sh/ruff/pull/10394))
### CLI
- Add message on success to `ruff check` ([#8631](https://github.com/astral-sh/ruff/pull/8631))
### Bug fixes
- \[`PIE970`\] Allow trailing ellipsis in `typing.TYPE_CHECKING` ([#10413](https://github.com/astral-sh/ruff/pull/10413))
- Avoid `TRIO115` if the argument is a variable ([#10376](https://github.com/astral-sh/ruff/pull/10376))
- \[`F811`\] Avoid removing shadowed imports that point to different symbols ([#10387](https://github.com/astral-sh/ruff/pull/10387))
- Fix `F821` and `F822` false positives in `.pyi` files ([#10341](https://github.com/astral-sh/ruff/pull/10341))
- Fix `F821` false negatives in `.py` files when `from __future__ import annotations` is active ([#10362](https://github.com/astral-sh/ruff/pull/10362))
- Fix case where `Indexer` fails to identify continuation preceded by newline #10351 ([#10354](https://github.com/astral-sh/ruff/pull/10354))
- Sort hash maps in `Settings` display ([#10370](https://github.com/astral-sh/ruff/pull/10370))
- Track conditional deletions in the semantic model ([#10415](https://github.com/astral-sh/ruff/pull/10415))
- \[`C413`\] Wrap expressions in parentheses when negating ([#10346](https://github.com/astral-sh/ruff/pull/10346))
- \[`pycodestyle`\] Do not ignore lines before the first logical line in blank lines rules. ([#10382](https://github.com/astral-sh/ruff/pull/10382))
- \[`pycodestyle`\] Do not trigger `E225` and `E275` when the next token is a ')' ([#10315](https://github.com/astral-sh/ruff/pull/10315))
- \[`pylint`\] Avoid false-positive slot non-assignment for `__dict__` (`PLE0237`) ([#10348](https://github.com/astral-sh/ruff/pull/10348))
- Gate f-string struct size test for Rustc \< 1.76 ([#10371](https://github.com/astral-sh/ruff/pull/10371))
### Documentation
- Use `ruff.toml` format in README ([#10393](https://github.com/astral-sh/ruff/pull/10393))
- \[`RUF008`\] Make it clearer that a mutable default in a dataclass is only valid if it is typed as a ClassVar ([#10395](https://github.com/astral-sh/ruff/pull/10395))
- \[`pylint`\] Extend docs and test in `invalid-str-return-type` (`E307`) ([#10400](https://github.com/astral-sh/ruff/pull/10400))
- Remove `.` from `check` and `format` commands ([#10217](https://github.com/astral-sh/ruff/pull/10217))
## 0.3.2
### Preview features
- Improve single-`with` item formatting for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
### Rule changes
- \[`pyupgrade`\] Allow fixes for f-string rule regardless of line length (`UP032`) ([#10263](https://github.com/astral-sh/ruff/pull/10263))
- \[`pycodestyle`\] Include actual conditions in E712 diagnostics ([#10254](https://github.com/astral-sh/ruff/pull/10254))
### Bug fixes
- Fix trailing kwargs end of line comment after slash ([#10297](https://github.com/astral-sh/ruff/pull/10297))
- Fix unstable `with` items formatting ([#10274](https://github.com/astral-sh/ruff/pull/10274))
- Avoid repeating function calls in f-string conversions ([#10265](https://github.com/astral-sh/ruff/pull/10265))
- Fix E203 false positive for slices in format strings ([#10280](https://github.com/astral-sh/ruff/pull/10280))
- Fix incorrect `Parameter` range for `*args` and `**kwargs` ([#10283](https://github.com/astral-sh/ruff/pull/10283))
- Treat `typing.Annotated` subscripts as type definitions ([#10285](https://github.com/astral-sh/ruff/pull/10285))
## 0.3.1
### Preview features
- \[`pycodestyle`\] Fix E301 not triggering on decorated methods. ([#10117](https://github.com/astral-sh/ruff/pull/10117))
- \[`pycodestyle`\] Respect `isort` settings in blank line rules (`E3*`) ([#10096](https://github.com/astral-sh/ruff/pull/10096))
- \[`pycodestyle`\] Make blank lines in typing stub files optional (`E3*`) ([#10098](https://github.com/astral-sh/ruff/pull/10098))
- \[`pylint`\] Implement `singledispatch-method` (`E1519`) ([#10140](https://github.com/astral-sh/ruff/pull/10140))
- \[`pylint`\] Implement `useless-exception-statement` (`W0133`) ([#10176](https://github.com/astral-sh/ruff/pull/10176))
### Rule changes
- \[`flake8-debugger`\] Check for use of `debugpy` and `ptvsd` debug modules (#10177) ([#10194](https://github.com/astral-sh/ruff/pull/10194))
- \[`pyupgrade`\] Generate diagnostic for all valid f-string conversions regardless of line length (`UP032`) ([#10238](https://github.com/astral-sh/ruff/pull/10238))
- \[`pep8_naming`\] Add fixes for `N804` and `N805` ([#10215](https://github.com/astral-sh/ruff/pull/10215))
### CLI
- Colorize the output of `ruff format --diff` ([#10110](https://github.com/astral-sh/ruff/pull/10110))
- Make `--config` and `--isolated` global flags ([#10150](https://github.com/astral-sh/ruff/pull/10150))
- Correctly expand tildes and environment variables in paths passed to `--config` ([#10219](https://github.com/astral-sh/ruff/pull/10219))
### Configuration
- Accept a PEP 440 version specifier for `required-version` ([#10216](https://github.com/astral-sh/ruff/pull/10216))
- Implement isort's `default-section` setting ([#10149](https://github.com/astral-sh/ruff/pull/10149))
### Bug fixes
- Remove trailing space from `CapWords` message ([#10220](https://github.com/astral-sh/ruff/pull/10220))
- Respect external codes in file-level exemptions ([#10203](https://github.com/astral-sh/ruff/pull/10203))
- \[`flake8-raise`\] Avoid false-positives for parens-on-raise with `future.exception()` (`RSE102`) ([#10206](https://github.com/astral-sh/ruff/pull/10206))
- \[`pylint`\] Add fix for unary expressions in `PLC2801` ([#9587](https://github.com/astral-sh/ruff/pull/9587))
- \[`ruff`\] Fix RUF028 not allowing `# fmt: skip` on match cases ([#10178](https://github.com/astral-sh/ruff/pull/10178))
## 0.3.0
This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.
### Preview features
- \[`flake8-bandit`\] Remove suspicious-lxml-import (`S410`) ([#10154](https://github.com/astral-sh/ruff/pull/10154))
- \[`pycodestyle`\] Allow `os.environ` modifications between imports (`E402`) ([#10066](https://github.com/astral-sh/ruff/pull/10066))
- \[`pycodestyle`\] Don't warn about a single whitespace character before a comma in a tuple (`E203`) ([#10094](https://github.com/astral-sh/ruff/pull/10094))
### Rule changes
- \[`eradicate`\] Detect commented out `case` statements (`ERA001`) ([#10055](https://github.com/astral-sh/ruff/pull/10055))
- \[`eradicate`\] Detect single-line code for `try:`, `except:`, etc. (`ERA001`) ([#10057](https://github.com/astral-sh/ruff/pull/10057))
- \[`flake8-boolean-trap`\] Allow boolean positionals in `__post_init__` ([#10027](https://github.com/astral-sh/ruff/pull/10027))
- \[`flake8-copyright`\] Allow © in copyright notices ([#10065](https://github.com/astral-sh/ruff/pull/10065))
- \[`isort`\]: Use one blank line after imports in typing stub files ([#9971](https://github.com/astral-sh/ruff/pull/9971))
- \[`pylint`\] New Rule `dict-iter-missing-items` (`PLE1141`) ([#9845](https://github.com/astral-sh/ruff/pull/9845))
- \[`pylint`\] Ignore `sys.version` and `sys.platform` (`PLR1714`) ([#10054](https://github.com/astral-sh/ruff/pull/10054))
- \[`pyupgrade`\] Detect literals with unary operators (`UP018`) ([#10060](https://github.com/astral-sh/ruff/pull/10060))
- \[`ruff`\] Expand rule for `list(iterable).pop(0)` idiom (`RUF015`) ([#10148](https://github.com/astral-sh/ruff/pull/10148))
### Formatter
This release introduces the Ruff 2024.2 style, stabilizing the following changes:
- Prefer splitting the assignment's value over the target or type annotation ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- Remove blank lines before class docstrings ([#9154](https://github.com/astral-sh/ruff/pull/9154))
- Wrap multiple context managers in `with` parentheses when targeting Python 3.9 or newer ([#9222](https://github.com/astral-sh/ruff/pull/9222))
- Add a blank line after nested classes with a dummy body (`...`) in typing stub files ([#9155](https://github.com/astral-sh/ruff/pull/9155))
- Reduce vertical spacing for classes and functions with a dummy (`...`) body ([#7440](https://github.com/astral-sh/ruff/issues/7440), [#9240](https://github.com/astral-sh/ruff/pull/9240))
- Add a blank line after the module docstring ([#8283](https://github.com/astral-sh/ruff/pull/8283))
- Parenthesize long type hints in assignments ([#9210](https://github.com/astral-sh/ruff/pull/9210))
- Preserve indent for single multiline-string call-expressions ([#9673](https://github.com/astral-sh/ruff/pull/9637))
- Normalize hex escape and unicode escape sequences ([#9280](https://github.com/astral-sh/ruff/pull/9280))
- Format module docstrings ([#9725](https://github.com/astral-sh/ruff/pull/9725))
### CLI
- Explicitly disallow `extend` as part of a `--config` flag ([#10135](https://github.com/astral-sh/ruff/pull/10135))
- Remove `build` from the default exclusion list ([#10093](https://github.com/astral-sh/ruff/pull/10093))
- Deprecate `ruff <path>`, `ruff --explain`, `ruff --clean`, and `ruff --generate-shell-completion` in favor of `ruff check <path>`, `ruff rule`, `ruff clean`, and `ruff generate-shell-completion` ([#10169](https://github.com/astral-sh/ruff/pull/10169))
- Remove the deprecated CLI option `--format` from `ruff rule` and `ruff linter` ([#10170](https://github.com/astral-sh/ruff/pull/10170))
### Bug fixes
- \[`flake8-bugbear`\] Avoid adding default initializers to stubs (`B006`) ([#10152](https://github.com/astral-sh/ruff/pull/10152))
- \[`flake8-type-checking`\] Respect runtime-required decorators for function signatures ([#10091](https://github.com/astral-sh/ruff/pull/10091))
- \[`pycodestyle`\] Mark fixes overlapping with a multiline string as unsafe (`W293`) ([#10049](https://github.com/astral-sh/ruff/pull/10049))
- \[`pydocstyle`\] Trim whitespace when removing blank lines after section (`D413`) ([#10162](https://github.com/astral-sh/ruff/pull/10162))
- \[`pylint`\] Delete entire statement, including semicolons (`PLR0203`) ([#10074](https://github.com/astral-sh/ruff/pull/10074))
- \[`ruff`\] Avoid f-string false positives in `gettext` calls (`RUF027`) ([#10118](https://github.com/astral-sh/ruff/pull/10118))
- Fix `ruff` crashing on PowerPC systems because of too small page size ([#10080](https://github.com/astral-sh/ruff/pull/10080))
### Performance
- Add cold attribute to less likely printer queue branches in the formatter ([#10121](https://github.com/astral-sh/ruff/pull/10121))
- Skip unnecessary string normalization in the formatter ([#10116](https://github.com/astral-sh/ruff/pull/10116))
### Documentation
- Remove "Beta" Label from formatter documentation ([#10144](https://github.com/astral-sh/ruff/pull/10144))
- `line-length` option: fix link to `pycodestyle.max-line-length` ([#10136](https://github.com/astral-sh/ruff/pull/10136))
## 0.2.2
Highlights include:
- Initial support formatting f-strings (in `--preview`).
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config` argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
### Preview features
- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642))
- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266))
- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880))
### Rule changes
- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793))
- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862))
- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887))
- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976))
- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952))
### Formatter
- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915))
- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957))
- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922))
### CLI
- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599))
### Bug fixes
- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866))
- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905))
- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969))
- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981))
- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953))
- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996))
- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859))
- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955))
- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907))
- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894))
- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878))
- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964))
- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956))
- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865))
- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990))
- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849))
### Performance
- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888))
- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853))
- Reduce `Result<Tok, LexicalError>` size by using `Box<str>` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885))
- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900))
- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867))
- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884))
## 0.2.1
This release includes support for range formatting (i.e., the ability to format specific lines
@@ -1084,7 +593,7 @@ docstrings via the `docstring-code-format` setting.
- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998))
- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812))
- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
### Bug fixes
@@ -1213,7 +722,7 @@ docstrings via the `docstring-code-format` setting.
- \[`flake8-trio`\] Implement `TRIO115` ([#8486](https://github.com/astral-sh/ruff/pull/8486))
- \[`refurb`\] Implement `type-none-comparison` (`FURB169`) ([#8487](https://github.com/astral-sh/ruff/pull/8487))
- Flag all comparisons against builtin types in `E721` ([#8491](https://github.com/astral-sh/ruff/pull/8491))
- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525))
- Make `SIM118` fix as safe when the expression is a known dictionary ([#8525](https://github.com/astral-sh/ruff/pull/8525))
### Formatter
@@ -1381,7 +890,7 @@ Try it today with `ruff format`! [Check out the blog post](https://astral.sh/blo
- Add `backports.strenum` to `deprecated-imports` ([#8113](https://github.com/astral-sh/ruff/pull/8113))
- Update `SIM112` to ignore `https_proxy`, `http_proxy`, and `no_proxy` ([#8140](https://github.com/astral-sh/ruff/pull/8140))
- Update fix for `literal-membership` (`PLR6201`) to be unsafe ([#8097](https://github.com/astral-sh/ruff/pull/8097))
- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108))
- Update fix for `mutable-argument-defaults` (`B006`) to be unsafe ([#8108](https://github.com/astral-sh/ruff/pull/8108))
### Formatter
@@ -1509,7 +1018,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).*
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
### Configuration

View File

@@ -33,18 +33,27 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
## The Basics
Ruff welcomes contributions in the form of pull requests.
Ruff welcomes contributions in the form of Pull Requests.
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
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)
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)
that are ready for contributions.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules from the [`flake8-pyi`](https://github.com/astral-sh/ruff/issues/848)
plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for
guidance.
If you have suggestions on how we might improve the contributing documentation, [let us know](https://github.com/astral-sh/ruff/discussions/5693)!
### Prerequisites
@@ -98,7 +107,7 @@ RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your pull request, but running them locally
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
will save you time and expedite the merge process.
Note that many code changes also require updating the snapshot tests, which is done interactively
@@ -108,14 +117,7 @@ after running `cargo test` like so:
cargo insta review
```
If your pull request relates to a specific lint rule, include the category and rule code in the
title, as in the following examples:
- \[`flake8-bugbear`\] Avoid false positive for usage after `continue` (`B031`)
- \[`flake8-simplify`\] Detect implicit `else` cases in `needless-bool` (`SIM103`)
- \[`pycodestyle`\] Implement `redundant-backslash` (`E502`)
Your pull request will be reviewed by a maintainer, which may involve a few rounds of iteration
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
### Project Structure
@@ -123,8 +125,8 @@ prior to merging.
Ruff is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html),
such that all crates are contained in a flat `crates` directory.
The vast majority of the code, including all lint rules, lives in the `ruff_linter` crate (located
at `crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you.
The vast majority of the code, including all lint rules, lives in the `ruff` crate (located at
`crates/ruff_linter`). As a contributor, that's the crate that'll be most relevant to you.
At the time of writing, the repository includes the following crates:
@@ -197,14 +199,11 @@ and calling out to lint rule analyzer functions as it goes.
If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. Grep
for the `Diagnostic::new` invocations to understand how other, similar rules are implemented.
Once you're satisfied with your code, add tests for your rule
(see: [rule testing](#rule-testing-fixtures-and-snapshots)), and regenerate the documentation and
associated assets (like our JSON Schema) with `cargo dev generate-all`.
Once you're satisfied with your code, add tests for your rule. See [rule testing](#rule-testing-fixtures-and-snapshots)
for more details.
Finally, submit a pull request, and include the category, rule name, and rule code in the title, as
in:
> \[`pycodestyle`\] Implement `redundant-backslash` (`E502`)
Finally, regenerate the documentation and other generated assets (like our JSON Schema) with:
`cargo dev generate-all`.
#### Rule naming convention
@@ -317,7 +316,7 @@ To preview any changes to the documentation locally:
```
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
@@ -330,13 +329,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
### Creating a new release
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
1. Run `./scripts/release/bump.sh`; this command will:
- Generate a temporary virtual environment with `rooster`
We use an experimental in-house tool for managing releases.
1. Install `rooster`: `pip install git+https://github.com/zanieb/rooster@main`
1. Run `rooster release`; this command will:
- 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
@@ -360,7 +359,7 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Open the draft release in the GitHub release section
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. Generate the contributor list with `rooster contributors` and add to the release notes
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.
@@ -814,8 +813,8 @@ To understand Ruff's import categorization system, we first need to define two c
"project root".)
- "Package root": The top-most directory defining the Python package that includes a given Python
file. To find the package root for a given Python file, traverse up its parent directories until
you reach a parent directory that doesn't contain an `__init__.py` file (and isn't in a subtree
marked as a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory
you reach a parent directory that doesn't contain an `__init__.py` file (and isn't marked as
a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory
just before that, i.e., the first directory in the package.
For example, given:

1025
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,109 +12,103 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.3" }
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.80" }
argfile = { version = "0.2.0" }
anyhow = { version = "1.0.79" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.5.0" }
bstr = { version = "1.9.1" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "3.0.0" }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", 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" }
countme = { version ="3.0.1"}
criterion = { version = "0.5.1", default-features = false }
crossbeam-channel = { version = "0.5.12" }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
env_logger = { version ="0.10.1"}
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version = "2.11.0" }
fs-err = { version ="2.11.0"}
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hexf-parse = { version = "0.2.1" }
hexf-parse = { version ="0.2.1"}
ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imara-diff ={ version = "0.1.5"}
imperative = { version = "1.0.4" }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.6.0" }
indicatif ={ version = "0.17.8"}
indoc ={ version = "2.0.4"}
insta = { version = "1.34.0", 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" }
js-sys = { version = "0.3.67" }
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 }
log = { version = "0.4.17" }
lsp-server = { version = "0.7.6" }
lsp-types = { version = "0.95.0", features = ["proposed"] }
matchit = { version = "0.8.1" }
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
mimalloc = { version ="0.1.39"}
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
num_cpus = { version = "1.16.0" }
once_cell = { version = "1.19.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.6.0", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
proc-macro2 = { version = "1.0.78" }
pyproject-toml = { version = "0.9.0" }
quick-junit = { version = "0.4.0" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rayon = { version = "1.10.0" }
rayon = { version = "1.8.1" }
regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.21" }
serde = { version = "1.0.196", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
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" }
shlex = { version = "1.3.0" }
shlex = { version ="1.3.0"}
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2" }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0"
strum = { version = "0.26.0", features = ["strum_macros"] }
strum_macros = { version = "0.26.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.9.0"}
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.58" }
tikv-jemallocator = { version = "0.5.0" }
toml = { version = "0.8.11" }
thiserror = { version = "1.0.57" }
tikv-jemallocator = { version ="0.5.0"}
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-tree = { version = "0.3.0" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unic-ucd-category = { version ="0.9"}
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
ureq = { version = "2.9.6" }
unicode_names2 = { version = "1.2.1" }
ureq = { version = "2.9.1" }
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.42" }
wasm-bindgen = { version = "0.2.84" }
wasm-bindgen-test = { version = "0.3.40" }
wild = { version = "2" }
[workspace.lints.rust]

View File

@@ -4,12 +4,11 @@
[![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)
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](LICENSE)
[![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)
[![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.
@@ -32,7 +31,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8), isort, and Black
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
- 📏 Over [700 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations
of popular Flake8 plugins, like flake8-bugbear
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/integrations/) for
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
@@ -50,7 +49,6 @@ times faster than any individual tool.
Ruff is extremely actively developed and used in major open-source projects like:
- [Apache Airflow](https://github.com/apache/airflow)
- [Apache Superset](https://github.com/apache/superset)
- [FastAPI](https://github.com/tiangolo/fastapi)
- [Hugging Face](https://github.com/huggingface/transformers)
- [Pandas](https://github.com/pandas-dev/pandas)
@@ -130,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:
```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/*.py # Lint all `.py` files in `/path/to/code`.
ruff check path/to/code/to/file.py # Lint `file.py`.
@@ -140,7 +138,7 @@ ruff check @arguments.txt # Lint using an input file, treating its con
Or, to run Ruff as a formatter:
```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/*.py # Format all `.py` files in `/path/to/code`.
ruff format path/to/code/to/file.py # Format `file.py`.
@@ -152,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.2
rev: v0.2.1
hooks:
# Run the linter.
- id: ruff
@@ -174,7 +172,7 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1
```
@@ -184,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/)
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
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
@@ -224,7 +223,7 @@ indent-width = 4
# Assume Python 3.8
target-version = "py38"
[lint]
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
@@ -236,7 +235,7 @@ unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format]
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
@@ -250,20 +249,11 @@ skip-magic-trailing-comma = false
line-ending = "auto"
```
Note that, in a `pyproject.toml`, each section header should be prefixed with `tool.ruff`. For
example, `[lint]` should be replaced with `[tool.ruff.lint]`.
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:
Some configuration options can be provided via the command-line, such as those related to
rule enablement and disablement, file discovery, and logging level:
```shell
ruff check --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']}"
ruff check path/to/code/ --select F401 --select F403 --quiet
```
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
@@ -273,7 +263,7 @@ for more on the linting and formatting commands, respectively.
<!-- Begin section: Rules -->
**Ruff supports over 800 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 700 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.
@@ -351,14 +341,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
[**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
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).
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
@@ -388,7 +378,6 @@ Ruff is released under the MIT license.
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))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow)
@@ -415,7 +404,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Ibis](https://github.com/ibis-project/ibis)
- [ivy](https://github.com/unifyai/ivy)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Kraken Tech](https://kraken.tech/)
- [LangChain](https://github.com/hwchase17/langchain)
- [Litestar](https://litestar.dev/)
- [LlamaIndex](https://github.com/jerryjliu/llama_index)
@@ -430,7 +418,6 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Mypy](https://github.com/python/mypy)
- Netflix ([Dispatch](https://github.com/Netflix/dispatch))
- [Neon](https://github.com/neondatabase/neon)
- [Nokia](https://nokia.com/)
- [NoneBot](https://github.com/nonebot/nonebot2)
- [NumPyro](https://github.com/pyro-ppl/numpyro)
- [ONNX](https://github.com/onnx/onnx)
@@ -499,7 +486,7 @@ If you're using Ruff, consider adding the Ruff badge to your project's `README.m
## License
This repository is licensed under the [MIT License](LICENSE)
MIT
<div align="center">
<a target="_blank" href="https://astral.sh" style="background:none">

View File

@@ -3,17 +3,9 @@
extend-exclude = ["**/resources/**/*", "**/snapshots/**/*"]
[default.extend-words]
"arange" = "arange" # e.g. `numpy.arange`
hel = "hel"
whos = "whos"
spawnve = "spawnve"
ned = "ned"
pn = "pn" # `import panel as pd` is a thing
poit = "poit"
BA = "BA" # acronym for "Bad Allowed", used in testing.
[default]
extend-ignore-re = [
# Line ignore with trailing "spellchecker:disable-line"
"(?Rm)^.*#\\s*spellchecker:disable-line$"
]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.4.2"
version = "0.2.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -20,7 +20,6 @@ ruff_macros = { path = "../ruff_macros" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_server = { path = "../ruff_server" }
ruff_source_file = { path = "../ruff_source_file" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_workspace = { path = "../ruff_workspace" }
@@ -41,7 +40,6 @@ is-macro = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
notify = { workspace = true }
num_cpus = { workspace = true }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { workspace = true }
regex = { workspace = true }
@@ -54,16 +52,15 @@ tempfile = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["registry"] }
tracing-tree = { workspace = true }
walkdir = { workspace = true }
wild = { workspace = true }
[dev-dependencies]
# Enable test rules during development
ruff_linter = { path = "../ruff_linter", features = ["clap", "test-rules"] }
assert_cmd = { workspace = true }
# 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-cmd = { workspace = true }
tempfile = { workspace = true }

View File

@@ -28,52 +28,6 @@ use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::{Options, PycodestyleOptions};
use ruff_workspace::resolver::ConfigurationTransformer;
/// All configuration options that can be passed "globally",
/// i.e., can be passed to all subcommands
#[derive(Debug, Default, Clone, clap::Args)]
pub struct GlobalConfigArgs {
#[clap(flatten)]
log_level_args: LogLevelArgs,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
global = true,
help_heading = "Global options",
)]
pub config: Vec<SingleConfigArgument>,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, help_heading = "Global options", global = true)]
pub isolated: bool,
}
impl GlobalConfigArgs {
pub fn log_level(&self) -> LogLevel {
LogLevel::from(&self.log_level_args)
}
#[must_use]
fn partition(self) -> (LogLevel, Vec<SingleConfigArgument>, bool) {
(self.log_level(), self.config, self.isolated)
}
}
#[derive(Debug, Parser)]
#[command(
author,
@@ -84,9 +38,9 @@ impl GlobalConfigArgs {
#[command(version)]
pub struct Args {
#[command(subcommand)]
pub(crate) command: Command,
pub command: Command,
#[clap(flatten)]
pub(crate) global_options: GlobalConfigArgs,
pub log_level_args: LogLevelArgs,
}
#[allow(clippy::large_enum_variant)]
@@ -109,6 +63,10 @@ pub enum Command {
/// 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> },
@@ -117,6 +75,10 @@ pub enum Command {
/// 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")]
@@ -126,8 +88,6 @@ pub enum Command {
GenerateShellCompletion { shell: clap_complete_command::Shell },
/// Run the Ruff formatter on the given files or directories.
Format(FormatCommand),
/// Run the language server.
Server(ServerCommand),
/// Display Ruff's version
Version {
#[arg(long, value_enum, default_value = "text")]
@@ -190,7 +150,7 @@ pub struct CheckCommand {
pub output_format: Option<SerializationFormat>,
/// Specify file to write the linter output to (default: stdout).
#[arg(short, long, env = "RUFF_OUTPUT_FILE")]
#[arg(short, long)]
pub output_file: Option<PathBuf>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
@@ -201,6 +161,20 @@ pub struct CheckCommand {
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
)]
pub config: Vec<SingleConfigArgument>,
/// Comma-separated list of rule codes to enable (or ALL, to enable all rules).
#[arg(
long,
@@ -332,6 +306,17 @@ pub struct CheckCommand {
/// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
pub no_cache: bool,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, 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>,
@@ -423,6 +408,20 @@ pub struct FormatCommand {
/// difference between the current file and how the formatted file would look like.
#[arg(long)]
pub diff: bool,
/// Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`),
/// or a TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ruff.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files, including configuration files that were also
/// specified using `--config`.
#[arg(
long,
action = clap::ArgAction::Append,
value_name = "CONFIG_OPTION",
value_parser = ConfigArgumentParser,
)]
pub config: Vec<SingleConfigArgument>,
/// Disable cache reads.
#[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")]
@@ -463,6 +462,17 @@ pub struct FormatCommand {
/// Set the line-length.
#[arg(long, help_heading = "Format configuration")]
pub line_length: Option<LineLength>,
/// Ignore all configuration files.
//
// Note: We can't mark this as conflicting with `--config` here
// as `--config` can be used for specifying configuration overrides
// as well as configuration files.
// Specifying a configuration file conflicts with `--isolated`;
// specifying a configuration override does not.
// If a user specifies `ruff check --isolated --config=ruff.toml`,
// we emit an error later on, after the initial parsing by clap.
#[arg(long, 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>,
@@ -496,13 +506,6 @@ pub struct FormatCommand {
pub range: Option<FormatRange>,
}
#[derive(Copy, Clone, Debug, clap::Parser)]
pub struct ServerCommand {
/// Enable preview mode; required for regular operation
#[arg(long)]
pub(crate) preview: bool,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum HelpFormat {
Text,
@@ -510,7 +513,7 @@ pub enum HelpFormat {
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, clap::Args)]
#[derive(Debug, clap::Args)]
pub struct LogLevelArgs {
/// Enable verbose logging.
#[arg(
@@ -558,10 +561,6 @@ impl From<&LogLevelArgs> for LogLevel {
/// Configuration-related arguments passed via the CLI.
#[derive(Default)]
pub struct ConfigArguments {
/// Whether the user specified --isolated on the command line
pub(crate) isolated: bool,
/// The logging level to be used, derived from command-line arguments passed
pub(crate) log_level: LogLevel,
/// Path to a pyproject.toml or ruff.toml configuration file (etc.).
/// Either 0 or 1 configuration file paths may be provided on the command line.
config_file: Option<PathBuf>,
@@ -582,19 +581,21 @@ impl ConfigArguments {
}
fn from_cli_arguments(
global_options: GlobalConfigArgs,
config_options: Vec<SingleConfigArgument>,
per_flag_overrides: ExplicitConfigOverrides,
isolated: bool,
) -> anyhow::Result<Self> {
let (log_level, config_options, isolated) = global_options.partition();
let mut config_file: Option<PathBuf> = None;
let mut overrides = Configuration::default();
let mut new = Self {
per_flag_overrides,
..Self::default()
};
for option in config_options {
match option {
SingleConfigArgument::SettingsOverride(overridden_option) => {
let overridden_option = Arc::try_unwrap(overridden_option)
.unwrap_or_else(|option| option.deref().clone());
overrides = overrides.combine(Configuration::from_options(
new.overrides = new.overrides.combine(Configuration::from_options(
overridden_option,
None,
&path_dedot::CWD,
@@ -613,7 +614,7 @@ The argument `--config={}` cannot be used with `--isolated`
path.display()
);
}
if let Some(ref config_file) = config_file {
if let Some(ref config_file) = new.config_file {
let (first, second) = (config_file.display(), path.display());
bail!(
"\
@@ -624,17 +625,11 @@ You cannot specify more than one configuration file on the command line.
"
);
}
config_file = Some(path);
new.config_file = Some(path);
}
}
}
Ok(Self {
isolated,
log_level,
config_file,
overrides,
per_flag_overrides,
})
Ok(new)
}
}
@@ -648,10 +643,7 @@ impl ConfigurationTransformer for ConfigArguments {
impl CheckCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(
self,
global_options: GlobalConfigArgs,
) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
pub fn partition(self) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
let check_arguments = CheckArguments {
add_noqa: self.add_noqa,
diff: self.diff,
@@ -660,6 +652,7 @@ impl CheckCommand {
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,
@@ -703,7 +696,8 @@ impl CheckCommand {
extension: self.extension,
};
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
let config_args =
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
Ok((check_arguments, config_args))
}
}
@@ -711,14 +705,12 @@ impl CheckCommand {
impl FormatCommand {
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(
self,
global_options: GlobalConfigArgs,
) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
pub fn partition(self) -> anyhow::Result<(FormatArguments, ConfigArguments)> {
let format_arguments = FormatArguments {
check: self.check,
diff: self.diff,
files: self.files,
isolated: self.isolated,
no_cache: self.no_cache,
stdin_filename: self.stdin_filename,
range: self.range,
@@ -738,7 +730,8 @@ impl FormatCommand {
..ExplicitConfigOverrides::default()
};
let config_args = ConfigArguments::from_cli_arguments(global_options, cli_overrides)?;
let config_args =
ConfigArguments::from_cli_arguments(self.config, cli_overrides, self.isolated)?;
Ok((format_arguments, config_args))
}
}
@@ -752,34 +745,38 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
}
}
/// Enumeration of various ways in which a --config CLI flag
/// could be invalid
#[derive(Debug)]
enum InvalidConfigFlagReason {
InvalidToml(toml::de::Error),
/// It was valid TOML, but not a valid ruff config file.
/// E.g. the user tried to select a rule that doesn't exist,
/// or tried to enable a setting that doesn't exist
ValidTomlButInvalidRuffSchema(toml::de::Error),
/// It was a valid ruff config file, but the user tried to pass a
/// value for `extend` as part of the config override.
// `extend` is special, because it affects which config files we look at
/// in the first place. We currently only parse --config overrides *after*
/// we've combined them with all the arguments from the various config files
/// that we found, so trying to override `extend` as part of a --config
/// override is forbidden.
ExtendPassedViaConfigFlag,
enum TomlParseFailureKind {
SyntaxError,
UnknownOption,
}
impl InvalidConfigFlagReason {
const fn description(&self) -> &'static str {
match self {
Self::InvalidToml(_) => "The supplied argument is not valid TOML",
Self::ValidTomlButInvalidRuffSchema(_) => {
impl std::fmt::Display for TomlParseFailureKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display = match self {
Self::SyntaxError => "The supplied argument is not valid TOML",
Self::UnknownOption => {
"Could not parse the supplied argument as a `ruff.toml` configuration option"
}
Self::ExtendPassedViaConfigFlag => "Cannot include `extend` in a --config flag value",
}
};
write!(f, "{display}")
}
}
#[derive(Debug)]
struct TomlParseFailure {
kind: TomlParseFailureKind,
underlying_error: toml::de::Error,
}
impl std::fmt::Display for TomlParseFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let TomlParseFailure {
kind,
underlying_error,
} = self;
let display = format!("{kind}:\n\n{underlying_error}");
write!(f, "{}", display.trim_end())
}
}
@@ -821,38 +818,27 @@ impl TypedValueParser for ConfigArgumentParser {
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
// Convert to UTF-8.
let Some(value) = value.to_str() else {
// But respect non-UTF-8 paths.
let path_to_config_file = PathBuf::from(value);
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
return Err(clap::Error::new(clap::error::ErrorKind::InvalidUtf8));
};
// Expand environment variables and tildes.
if let Ok(path_to_config_file) =
shellexpand::full(value).map(|config| PathBuf::from(&*config))
{
if path_to_config_file.is_file() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
let path_to_config_file = PathBuf::from(value);
if path_to_config_file.exists() {
return Ok(SingleConfigArgument::FilePath(path_to_config_file));
}
let config_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into::<Options>() {
Ok(option) => {
if option.extend.is_none() {
return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option)));
}
InvalidConfigFlagReason::ExtendPassedViaConfigFlag
}
Err(underlying_error) => {
InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error)
}
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
let toml_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into() {
Ok(option) => return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option))),
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::UnknownOption,
underlying_error,
},
},
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::SyntaxError,
underlying_error,
},
Err(underlying_error) => InvalidConfigFlagReason::InvalidToml(underlying_error),
};
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
@@ -867,21 +853,6 @@ impl TypedValueParser for ConfigArgumentParser {
clap::error::ContextValue::String(value.to_string()),
);
let underlying_error = match &config_parse_error {
InvalidConfigFlagReason::ExtendPassedViaConfigFlag => {
let tip = config_parse_error.description().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
return Err(new_error);
}
InvalidConfigFlagReason::InvalidToml(underlying_error)
| InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error) => {
underlying_error
}
};
// small hack so that multiline tips
// have the same indent on the left-hand side:
let tip_indent = " ".repeat(" tip: ".len());
@@ -906,20 +877,16 @@ A `--config` flag must either be a path to a `.toml` configuration file
"
It looks like you were trying to pass a path to a configuration file.
The path `{value}` does not point to a configuration file"
The path `{value}` does not exist"
));
}
} else if value.contains('=') {
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
tip.push_str(&format!("\n\n{toml_parse_error}"));
}
let tip = tip.trim_end().to_owned().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
clap::error::ContextValue::StyledStrs(vec![tip.into()]),
);
Err(new_error)
@@ -974,6 +941,7 @@ pub struct CheckArguments {
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,
@@ -991,6 +959,7 @@ pub struct FormatArguments {
pub no_cache: bool,
pub diff: bool,
pub files: Vec<PathBuf>,
pub isolated: bool,
pub stdin_filename: Option<PathBuf>,
pub range: Option<FormatRange>,
}

View File

@@ -375,17 +375,15 @@ pub(crate) fn init(path: &Path) -> Result<()> {
fs::create_dir_all(path.join(VERSION))?;
// Add the CACHEDIR.TAG.
cachedir::ensure_tag(path)?;
if !cachedir::is_tagged(path)? {
cachedir::add_tag(path)?;
}
// Add the .gitignore.
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path.join(".gitignore"))
{
Ok(mut file) => file.write_all(b"# Automatically created by ruff.\n*\n")?,
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
Err(err) => return Err(err.into()),
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
let mut file = fs::File::create(gitignore_path)?;
file.write_all(b"*")?;
}
Ok(())

View File

@@ -252,7 +252,6 @@ mod test {
for file in [&pyproject_toml, &python_file, &notebook] {
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o000)
.open(file)?;

View File

@@ -61,8 +61,13 @@ impl FormatMode {
pub(crate) fn format(
cli: FormatArguments,
config_arguments: &ConfigArguments,
log_level: LogLevel,
) -> Result<ExitStatus> {
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
let mode = FormatMode::from_cli(&cli);
let files = resolve_default_files(cli.files, false);
let (paths, resolver) = python_files_in_path(&files, &pyproject_config, config_arguments)?;
@@ -197,7 +202,7 @@ pub(crate) fn format(
}
// Report on the formatting changes.
if config_arguments.log_level >= LogLevel::Default {
if log_level >= LogLevel::Default {
if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?;
@@ -527,7 +532,7 @@ impl<'a> FormatResults<'a> {
})
.sorted_unstable_by_key(|(path, _, _)| *path)
{
write!(f, "{}", unformatted.diff(formatted, Some(path)).unwrap())?;
unformatted.diff(formatted, Some(path), f)?;
}
Ok(())

View File

@@ -23,7 +23,11 @@ pub(crate) fn format_stdin(
cli: &FormatArguments,
config_arguments: &ConfigArguments,
) -> Result<ExitStatus> {
let pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
let mut resolver = Resolver::new(&pyproject_config);
warn_incompatible_formatter_settings(&resolver);
@@ -118,13 +122,9 @@ fn format_source_code(
}
FormatMode::Check => {}
FormatMode::Diff => {
use std::io::Write;
write!(
&mut stdout().lock(),
"{}",
source_kind.diff(formatted, path).unwrap()
)
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
source_kind
.diff(formatted, path, &mut stdout().lock())
.map_err(|err| FormatCommandError::Diff(path.map(Path::to_path_buf), err))?;
}
},
FormattedSource::Unchanged => {

View File

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

View File

@@ -1,79 +0,0 @@
use std::num::NonZeroUsize;
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,
worker_threads: NonZeroUsize,
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(worker_threads)?;
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

@@ -3,7 +3,6 @@
use std::borrow::Cow;
use std::fs::File;
use std::io;
use std::io::Write;
use std::ops::{Add, AddAssign};
use std::path::Path;
@@ -290,10 +289,10 @@ pub(crate) fn lint_path(
match fix_mode {
flags::FixMode::Apply => transformed.write(&mut File::create(path)?)?,
flags::FixMode::Diff => {
write!(
source_kind.diff(
transformed.as_ref(),
Some(path),
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, Some(path)).unwrap()
)?;
}
flags::FixMode::Generate => {}
@@ -443,11 +442,7 @@ pub(crate) fn lint_stdin(
flags::FixMode::Diff => {
// But only write a diff if it's non-empty.
if !fixed.is_empty() {
write!(
&mut io::stdout().lock(),
"{}",
source_kind.diff(&transformed, path).unwrap()
)?;
source_kind.diff(transformed.as_ref(), path, &mut io::stdout().lock())?;
}
}
flags::FixMode::Generate => {}

View File

@@ -2,13 +2,11 @@
use std::fs::File;
use std::io::{self, stdout, BufWriter, Write};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use anyhow::Result;
use args::{GlobalConfigArgs, ServerCommand};
use clap::CommandFactory;
use colored::Colorize;
use log::warn;
@@ -20,7 +18,7 @@ use ruff_linter::settings::types::SerializationFormat;
use ruff_linter::{fs, warn_user, warn_user_once};
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};
pub mod args;
@@ -116,12 +114,20 @@ fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
}
}
/// Get the actual value of the `format` desired from either `output_format`
/// or `format`, and warn the user if they're using the deprecated form.
fn resolve_help_output_format(output_format: HelpFormat, format: Option<HelpFormat>) -> HelpFormat {
if format.is_some() {
warn_user!("The `--format` argument is deprecated. Use `--output-format` instead.");
}
format.unwrap_or(output_format)
}
pub fn run(
Args {
command,
global_options,
log_level_args,
}: Args,
deprecated_alias_warning: Option<&'static str>,
) -> Result<ExitStatus> {
{
let default_panic_hook = std::panic::take_hook();
@@ -149,18 +155,8 @@ pub fn run(
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
// support FORCE_COLOR env var
if let Some(force_color) = std::env::var_os("FORCE_COLOR") {
if force_color.len() > 0 {
colored::control::set_override(true);
}
}
set_up_logging(global_options.log_level())?;
if let Some(deprecated_alias_warning) = deprecated_alias_warning {
warn_user!("{}", deprecated_alias_warning);
}
let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
match command {
Command::Version { output_format } => {
@@ -170,8 +166,10 @@ pub fn run(
Command::Rule {
rule,
all,
output_format,
format,
mut output_format,
} => {
output_format = resolve_help_output_format(output_format, format);
if all {
commands::rule::rules(output_format)?;
}
@@ -184,51 +182,47 @@ pub fn run(
commands::config::config(option.as_deref())?;
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)?;
Ok(ExitStatus::Success)
}
Command::Clean => {
commands::clean::clean(global_options.log_level())?;
commands::clean::clean(log_level)?;
Ok(ExitStatus::Success)
}
Command::GenerateShellCompletion { shell } => {
shell.generate(&mut Args::command(), &mut stdout());
Ok(ExitStatus::Success)
}
Command::Check(args) => check(args, global_options),
Command::Format(args) => format(args, global_options),
Command::Server(args) => server(args, global_options.log_level()),
Command::Check(args) => check(args, log_level),
Command::Format(args) => format(args, log_level),
}
}
fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition()?;
if is_stdin(&cli.files, cli.stdin_filename.as_deref()) {
commands::format_stdin::format_stdin(&cli, &config_arguments)
} else {
commands::format::format(cli, &config_arguments)
commands::format::format(cli, &config_arguments, log_level)
}
}
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> {
let ServerCommand { preview } = args;
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
let worker_threads = num_cpus::get().max(4);
commands::server::run_server(
preview,
NonZeroUsize::try_from(worker_threads).expect("a non-zero worker thread count"),
log_level,
)
}
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition(global_options)?;
pub fn check(args: CheckCommand, log_level: LogLevel) -> Result<ExitStatus> {
let (cli, config_arguments) = args.partition()?;
// Construct the "default" settings. These are used when no `pyproject.toml`
// 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,
&config_arguments,
cli.stdin_filename.as_deref(),
)?;
let mut writer: Box<dyn Write> = match cli.output_file {
Some(path) if !cli.watch => {
@@ -319,7 +313,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
}
let modifications =
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?;
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
if modifications > 0 && log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" };
#[allow(clippy::print_stderr)]
{
@@ -331,7 +325,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
let printer = Printer::new(
output_format,
config_arguments.log_level,
log_level,
fix_mode,
unsafe_fixes,
printer_flags,
@@ -388,8 +382,11 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
};
if matches!(change_kind, ChangeKind::Configuration) {
pyproject_config =
resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?;
pyproject_config = resolve::resolve(
cli.isolated,
&config_arguments,
cli.stdin_filename.as_deref(),
)?;
}
Printer::clear_screen()?;
printer.write_to_user("File change detected...\n");

View File

@@ -27,42 +27,26 @@ pub fn main() -> ExitCode {
let mut args =
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
// and we also don't know if the user runs ruff with quiet.
// Keep the message and pass it to `run` that is responsible for emitting the warning.
let deprecated_alias_warning = match args.get(1).and_then(|arg| arg.to_str()) {
// Deprecated aliases that are handled by clap
Some("--explain") => {
Some("`ruff --explain <RULE>` is deprecated. Use `ruff rule <RULE>` instead.")
}
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)
// 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.
if let Some(arg) = args.get(1) {
if arg
.to_str()
.is_some_and(|arg| !Command::has_subcommand(rewrite_legacy_subcommand(arg)))
&& arg != "-h"
&& arg != "--help"
&& arg != "-V"
&& arg != "--version"
&& arg != "help" => {
{
args.insert(1, "check".into());
Some("`ruff <path>` is deprecated. Use `ruff check <path>` instead.")
}
},
_ => None
};
&& arg != "help"
{
args.insert(1, "check".into());
}
}
let args = Args::parse_from(args);
match run(args, deprecated_alias_warning) {
match run(args) {
Ok(code) => code.into(),
Err(err) => {
#[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

@@ -16,7 +16,7 @@ impl std::fmt::Display for PanicError {
}
thread_local! {
static LAST_PANIC: std::cell::Cell<Option<PanicError>> = const { std::cell::Cell::new(None) };
static LAST_PANIC: std::cell::Cell<Option<PanicError>> = std::cell::Cell::new(None);
}
/// [`catch_unwind`](std::panic::catch_unwind) wrapper that sets a custom [`set_hook`](std::panic::set_hook)

View File

@@ -118,8 +118,6 @@ impl Printer {
} else if remaining > 0 {
let s = if remaining == 1 { "" } else { "s" };
writeln!(writer, "Found {remaining} error{s}.")?;
} else if remaining == 0 {
writeln!(writer, "All checks passed!")?;
}
if let Some(fixables) = fixables {

View File

@@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::Result;
use log::debug;
@@ -16,11 +16,12 @@ use crate::args::ConfigArguments;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
pub fn resolve(
isolated: bool,
config_arguments: &ConfigArguments,
stdin_filename: Option<&Path>,
) -> Result<PyprojectConfig> {
// 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 settings = config.into_settings(&path_dedot::CWD)?;
debug!("Isolated mode, not reading any pyproject.toml");
@@ -34,8 +35,13 @@ pub fn resolve(
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
if let Some(pyproject) = config_arguments.config_file() {
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?;
if let Some(pyproject) = config_arguments
.config_file()
.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, config_arguments)?;
debug!(
"Using user-specified configuration file at: {}",
pyproject.display()
@@ -43,7 +49,7 @@ pub fn resolve(
return Ok(PyprojectConfig::new(
PyprojectDiscoveryStrategy::Fixed,
settings,
Some(pyproject.to_path_buf()),
Some(pyproject),
));
}

View File

@@ -70,7 +70,7 @@ pub(crate) fn version() -> VersionInfo {
#[cfg(test)]
mod tests {
use insta::{assert_json_snapshot, assert_snapshot};
use insta::{assert_display_snapshot, assert_json_snapshot};
use super::{CommitInfo, VersionInfo};
@@ -80,7 +80,7 @@ mod tests {
version: "0.0.0".to_string(),
commit_info: None,
};
assert_snapshot!(version);
assert_display_snapshot!(version);
}
#[test]
@@ -95,7 +95,7 @@ mod tests {
commits_since_last_tag: 0,
}),
};
assert_snapshot!(version);
assert_display_snapshot!(version);
}
#[test]
@@ -110,7 +110,7 @@ mod tests {
commits_since_last_tag: 24,
}),
};
assert_snapshot!(version);
assert_display_snapshot!(version);
}
#[test]

View File

@@ -12,10 +12,9 @@ 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");
cmd.arg("--output-format");
cmd.arg(output_format);
cmd.arg("--no-cache");
match show_source {
Some(true) => {
cmd.arg("--show-source");

View File

@@ -7,15 +7,10 @@ use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use regex::escape;
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
}
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
@@ -23,7 +18,7 @@ fn default_options() {
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print('Shouldn\'t change quotes')
print('Should\'t change quotes')
if condition:
@@ -38,7 +33,7 @@ if condition:
arg1,
arg2,
):
print("Shouldn't change quotes")
print("Should't change quotes")
if condition:
@@ -111,7 +106,7 @@ fn nonexistent_config_file() {
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not point to a configuration file
The path `foo.toml` does not exist
For more information, try '--help'.
"###);
@@ -152,29 +147,28 @@ fn too_many_config_files() -> Result<()> {
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
let expected_stderr = format!(
"\
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config={}` or `--config={}`.
For more information, try `--help`.
",
ruff_dot_toml.display(),
ruff2_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
Ok(())
}
@@ -183,29 +177,27 @@ fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
let expected_stderr = format!(
"\
ruff failed
Cause: The argument `--config={}` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
",
ruff_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--isolated")
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
Ok(())
}
@@ -358,52 +350,58 @@ def f(x):
'''
pass
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
.. code-block:: python
.. code-block:: python
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
)
Another example:
```py
foo, bar, quux = (
this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
And another:
Another example:
>>> foo, bar, quux = (
... this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
... )
"""
pass
```py
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
----- stderr -----
"###);
And another:
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
Ok(())
}
@@ -523,7 +521,7 @@ from module import =
----- stdout -----
----- stderr -----
error: Failed to parse main.py:2:20: Expected an import name
error: Failed to parse main.py:2:20: Unexpected token '='
"###);
Ok(())

View File

@@ -71,7 +71,6 @@ impl<'a> RuffCheck<'a> {
/// Generate a [`Command`] for the `ruff check` command.
fn build(self) -> Command {
let mut cmd = ruff_cmd();
cmd.arg("check");
if let Some(output_format) = self.output_format {
cmd.args(["--output-format", output_format]);
}
@@ -101,7 +100,6 @@ fn stdin_success() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -223,7 +221,6 @@ fn stdin_source_type_pyi() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -592,7 +589,6 @@ fn stdin_fix_when_no_issues_should_still_print_contents() {
print(sys.version)
----- stderr -----
All checks passed!
"###);
}
@@ -731,11 +727,11 @@ fn stdin_parse_error() {
success: false
exit_code: 1
----- stdout -----
-:1:17: E999 SyntaxError: Expected an import name
-:1:17: E999 SyntaxError: Unexpected token '='
Found 1 error.
----- stderr -----
error: Failed to parse at 1:17: Expected an import name
error: Failed to parse at 1:17: Unexpected token '='
"###);
}
@@ -809,13 +805,13 @@ fn full_output_format() {
}
#[test]
fn rule_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401"]));
fn explain_status_codes_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "F401"]));
}
#[test]
fn rule_invalid_rule_name() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404"]), @r###"
fn explain_status_codes_ruf404() {
assert_cmd_snapshot!(ruff_cmd().args(["--explain", "RUF404"]), @r###"
success: false
exit_code: 2
----- stdout -----
@@ -1026,7 +1022,6 @@ fn preview_disabled_direct() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Selection `RUF911` has no effect because preview is not enabled.
@@ -1043,7 +1038,6 @@ fn preview_disabled_prefix_empty() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Selection `RUF91` has no effect because preview is not enabled.
@@ -1060,7 +1054,6 @@ fn preview_disabled_does_not_warn_for_empty_ignore_selections() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1076,7 +1069,6 @@ fn preview_disabled_does_not_warn_for_empty_fixable_selections() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1182,7 +1174,6 @@ fn removed_indirect() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1213,7 +1204,6 @@ fn redirect_indirect() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1316,7 +1306,6 @@ fn deprecated_indirect_preview_enabled() {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -1353,14 +1342,13 @@ fn unreadable_pyproject_toml() -> Result<()> {
// Create an empty file with 000 permissions
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o000)
.open(pyproject_toml)?;
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args, None).err().context("Unexpected success")?;
let err = run(args).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)
@@ -1394,7 +1382,6 @@ fn unreadable_dir() -> Result<()> {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: Encountered error: Permission denied (os error 13)
@@ -1909,7 +1896,6 @@ def log(x, base) -> float:
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###

View File

@@ -12,7 +12,7 @@ use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
const STDIN_BASE_OPTIONS: &[&str] = &["check", "--no-cache", "--output-format", "concise"];
const STDIN_BASE_OPTIONS: &[&str] = &["--no-cache", "--output-format", "concise"];
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
@@ -246,6 +246,7 @@ OTHER = "OTHER"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
@@ -292,6 +293,7 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -384,6 +386,7 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -432,6 +435,7 @@ inline-quotes = "single"
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
@@ -491,12 +495,12 @@ ignore = ["D203", "D212"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(sub_dir)
.arg("check")
.args(STDIN_BASE_OPTIONS)
, @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
warning: No Python files found under the given path(s)
@@ -523,7 +527,7 @@ fn nonexistent_config_file() {
option
It looks like you were trying to pass a path to a configuration file.
The path `foo.toml` does not point to a configuration file
The path `foo.toml` does not exist
For more information, try '--help'.
"###);
@@ -591,24 +595,6 @@ fn too_many_config_files() -> Result<()> {
Ok(())
}
#[test]
fn extend_passed_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "extend = 'foo.toml'", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend = 'foo.toml'' for '--config <CONFIG_OPTION>'
tip: Cannot include `extend` in a --config flag value
For more information, try '--help'.
"###);
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -834,7 +820,6 @@ fn complex_config_setting_overridden_via_cli() -> Result<()> {
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
@@ -851,7 +836,7 @@ fn deprecated_config_option_overridden_via_cli() {
success: false
exit_code: 1
----- stdout -----
-:1:7: N801 Class name `lowercase` should use CapWords convention
-:1:7: N801 Class name `lowercase` should use CapWords convention
Found 1 error.
----- stderr -----
@@ -918,6 +903,7 @@ include = ["*.ipy"]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--extension", "ipy:ipynb"])
@@ -935,352 +921,3 @@ include = ["*.ipy"]
Ok(())
}
#[test]
fn file_noqa_external() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
external = ["AAA"]
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
# flake8: noqa: AAA101, BBB102
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:3:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
warning: Invalid rule code provided to `# ruff: noqa` at -:2: BBB102
"###);
});
Ok(())
}
#[test]
fn required_version_exact_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = "0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_exact_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = "{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
#[test]
fn required_version_bound_mismatch() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
format!(
r#"
required-version = ">{version}"
"#
),
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
"###);
});
Ok(())
}
#[test]
fn required_version_bound_match() -> Result<()> {
let version = env!("CARGO_PKG_VERSION");
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
required-version = ">=0.1.0"
"#,
)?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/"), (version, "[VERSION]")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
import os
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// Expand environment variables in `--config` paths provided via the CLI.
#[test]
fn config_expand() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
ruff_toml,
r#"
[lint]
select = ["F"]
ignore = ["F841"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg("${NAME}.toml")
.env("NAME", "ruff")
.arg("-")
.current_dir(tempdir.path())
.pass_stdin(r#"
import os
def func():
x = 1
"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:2:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
/// Per-file selects via ! negation in per-file-ignores
#[test]
fn negated_per_file_ignores() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.per-file-ignores]
"!selected.py" = ["RUF"]
"#,
)?;
let selected = tempdir.path().join("selected.py");
fs::write(selected, "")?;
let ignored = tempdir.path().join("ignored.py");
fs::write(ignored, "")?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("--select")
.arg("RUF901")
.current_dir(&tempdir)
, @r###"
success: false
exit_code: 1
----- stdout -----
selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn negated_per_file_ignores_absolute() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.per-file-ignores]
"!src/**.py" = ["RUF"]
"#,
)?;
let src_dir = tempdir.path().join("src");
fs::create_dir(&src_dir)?;
let selected = src_dir.join("selected.py");
fs::write(selected, "")?;
let ignored = tempdir.path().join("ignored.py");
fs::write(ignored, "")?;
insta::with_settings!({filters => vec![
// Replace windows paths
(r"\\", "/"),
]}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("--select")
.arg("RUF901")
.current_dir(&tempdir)
, @r###"
success: false
exit_code: 1
----- stdout -----
src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
});
Ok(())
}
/// patterns are additive, can't use negative patterns to "un-ignore"
#[test]
fn negated_per_file_ignores_overlap() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint.per-file-ignores]
"*.py" = ["RUF"]
"!foo.py" = ["RUF"]
"#,
)?;
let foo_file = tempdir.path().join("foo.py");
fs::write(foo_file, "")?;
let bar_file = tempdir.path().join("bar.py");
fs::write(bar_file, "")?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.arg("--select")
.arg("RUF901")
.current_dir(&tempdir)
, @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}

View File

@@ -3,7 +3,7 @@ source: crates/ruff/tests/integration_test.rs
info:
program: ruff
args:
- rule
- "--explain"
- F401
---
success: true
@@ -34,11 +34,6 @@ marking it as unused, as in:
from module import member as member
```
## Fix safety
When `ignore_init_module_imports` is disabled, fixes can remove for unused imports in `__init__` files.
These fixes are considered unsafe because they can change the public interface.
## Example
```python
import numpy as np # unused import
@@ -73,3 +68,4 @@ else:
- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
----- stderr -----

View File

@@ -44,16 +44,15 @@ file_resolver.exclude = [
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
file_resolver.extend_exclude = [
"crates/ruff/resources/",
"crates/ruff_linter/resources/",
"crates/ruff_python_formatter/resources/",
"crates/ruff_python_parser/resources/",
]
file_resolver.force_exclude = false
file_resolver.include = [
@@ -69,128 +68,128 @@ file_resolver.project_root = "[BASEPATH]"
linter.exclude = []
linter.project_root = "[BASEPATH]"
linter.rules.enabled = [
multiple-imports-on-one-line (E401),
module-import-not-at-top-of-file (E402),
multiple-statements-on-one-line-colon (E701),
multiple-statements-on-one-line-semicolon (E702),
useless-semicolon (E703),
none-comparison (E711),
true-false-comparison (E712),
not-in-test (E713),
not-is-test (E714),
type-comparison (E721),
bare-except (E722),
lambda-assignment (E731),
ambiguous-variable-name (E741),
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
syntax-error (E999),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),
late-future-import (F404),
undefined-local-with-import-star-usage (F405),
undefined-local-with-nested-import-star-usage (F406),
future-feature-not-defined (F407),
percent-format-invalid-format (F501),
percent-format-expected-mapping (F502),
percent-format-expected-sequence (F503),
percent-format-extra-named-arguments (F504),
percent-format-missing-argument (F505),
percent-format-mixed-positional-and-named (F506),
percent-format-positional-count-mismatch (F507),
percent-format-star-requires-sequence (F508),
percent-format-unsupported-format-character (F509),
string-dot-format-invalid-format (F521),
string-dot-format-extra-named-arguments (F522),
string-dot-format-extra-positional-arguments (F523),
string-dot-format-missing-arguments (F524),
string-dot-format-mixing-automatic (F525),
f-string-missing-placeholders (F541),
multi-value-repeated-key-literal (F601),
multi-value-repeated-key-variable (F602),
expressions-in-star-assignment (F621),
multiple-starred-expressions (F622),
assert-tuple (F631),
is-literal (F632),
invalid-print-syntax (F633),
if-tuple (F634),
break-outside-loop (F701),
continue-outside-loop (F702),
yield-outside-function (F704),
return-outside-function (F706),
default-except-not-last (F707),
forward-annotation-syntax-error (F722),
redefined-while-unused (F811),
undefined-name (F821),
undefined-export (F822),
undefined-local (F823),
unused-variable (F841),
unused-annotation (F842),
raise-not-implemented (F901),
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 = [
multiple-imports-on-one-line (E401),
module-import-not-at-top-of-file (E402),
multiple-statements-on-one-line-colon (E701),
multiple-statements-on-one-line-semicolon (E702),
useless-semicolon (E703),
none-comparison (E711),
true-false-comparison (E712),
not-in-test (E713),
not-is-test (E714),
type-comparison (E721),
bare-except (E722),
lambda-assignment (E731),
ambiguous-variable-name (E741),
ambiguous-class-name (E742),
ambiguous-function-name (E743),
io-error (E902),
syntax-error (E999),
unused-import (F401),
import-shadowed-by-loop-var (F402),
undefined-local-with-import-star (F403),
late-future-import (F404),
undefined-local-with-import-star-usage (F405),
undefined-local-with-nested-import-star-usage (F406),
future-feature-not-defined (F407),
percent-format-invalid-format (F501),
percent-format-expected-mapping (F502),
percent-format-expected-sequence (F503),
percent-format-extra-named-arguments (F504),
percent-format-missing-argument (F505),
percent-format-mixed-positional-and-named (F506),
percent-format-positional-count-mismatch (F507),
percent-format-star-requires-sequence (F508),
percent-format-unsupported-format-character (F509),
string-dot-format-invalid-format (F521),
string-dot-format-extra-named-arguments (F522),
string-dot-format-extra-positional-arguments (F523),
string-dot-format-missing-arguments (F524),
string-dot-format-mixing-automatic (F525),
f-string-missing-placeholders (F541),
multi-value-repeated-key-literal (F601),
multi-value-repeated-key-variable (F602),
expressions-in-star-assignment (F621),
multiple-starred-expressions (F622),
assert-tuple (F631),
is-literal (F632),
invalid-print-syntax (F633),
if-tuple (F634),
break-outside-loop (F701),
continue-outside-loop (F702),
yield-outside-function (F704),
return-outside-function (F706),
default-except-not-last (F707),
forward-annotation-syntax-error (F722),
redefined-while-unused (F811),
undefined-name (F821),
undefined-export (F822),
undefined-local (F823),
unused-variable (F841),
unused-annotation (F842),
raise-not-implemented (F901),
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 = []
@@ -203,7 +202,7 @@ 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.ignore_init_module_imports = false
linter.logger_objects = []
linter.namespace_packages = []
linter.src = [
@@ -233,7 +232,7 @@ 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}((-|,\s)\d{4})*
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
@@ -243,22 +242,7 @@ linter.flake8_gettext.functions_names = [
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.aliases = {"matplotlib": "mpl", "matplotlib.pyplot": "plt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "networkx": "nx", "plotly.express": "px", "polars": "pl", "numpy": "np", "panel": "pn", "pyarrow": "pa", "altair": "alt", "tkinter": "tk", "holoviews": "hv"}
linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = true
@@ -328,7 +312,6 @@ linter.isort.section_order = [
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
@@ -383,3 +366,4 @@ 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

@@ -65,7 +65,7 @@ use seahash::SeaHasher;
/// The main reason is that hashes and cache keys have different constraints:
///
/// * Cache keys are less performance sensitive: Hashes must be super fast to compute for performant hashed-collections. That's
/// why some standard types don't implement [`Hash`] where it would be safe to implement [`CacheKey`], e.g. `HashSet`
/// why some standard types don't implement [`Hash`] where it would be safe to to implement [`CacheKey`], e.g. `HashSet`
/// * Cache keys must be deterministic where hash keys do not have this constraint. That's why pointers don't implement [`CacheKey`] but they implement [`Hash`].
/// * Ideally, cache keys are portable
///

View File

@@ -22,7 +22,7 @@ ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"] }
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
anyhow = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
@@ -31,6 +31,7 @@ imara-diff = { workspace = true }
indicatif = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
once_cell = { workspace = true }
pretty_assertions = { workspace = true }
rayon = { workspace = true }
regex = { workspace = true }

View File

@@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, GlobalConfigArgs, LogLevelArgs};
use ruff::args::{ConfigArguments, FormatArguments, FormatCommand, LogLevelArgs};
use ruff::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel;
@@ -43,7 +43,7 @@ fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArgumen
.no_binary_name(true)
.get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, config_arguments) = arguments.partition(GlobalConfigArgs::default())?;
let (cli, config_arguments) = arguments.partition()?;
Ok((cli, config_arguments))
}
@@ -52,7 +52,11 @@ fn find_pyproject_config(
cli: &FormatArguments,
config_arguments: &ConfigArguments,
) -> anyhow::Result<PyprojectConfig> {
let mut pyproject_config = resolve(config_arguments, cli.stdin_filename.as_deref())?;
let mut pyproject_config = resolve(
cli.isolated,
config_arguments,
cli.stdin_filename.as_deref(),
)?;
// We don't want to format pyproject.toml
pyproject_config.settings.file_resolver.include = FilePatternSet::try_from_iter([
FilePattern::Builtin("*.py"),
@@ -134,7 +138,7 @@ impl Statistics {
}
}
/// We currently prefer the similarity index, but i'd like to keep this around
/// We currently prefer the the similarity index, but i'd like to keep this around
#[allow(clippy::cast_precision_loss, unused)]
pub(crate) fn jaccard_index(&self) -> f32 {
self.intersection as f32 / (self.black_input + self.ruff_output + self.intersection) as f32

View File

@@ -1,7 +1,6 @@
//! Generate Markdown documentation for applicable rules.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
@@ -98,13 +97,12 @@ pub(crate) fn main(args: &Args) -> Result<()> {
fn process_documentation(documentation: &str, out: &mut String, rule_name: &str) {
let mut in_options = false;
let mut after = String::new();
let mut referenced_options = HashSet::new();
// HACK: This is an ugly regex hack that's necessary because mkdocs uses
// a non-CommonMark-compliant Markdown parser, which doesn't support code
// tags in link definitions
// (see https://github.com/Python-Markdown/markdown/issues/280).
let documentation = Regex::new(r"\[`([^`]*?)`]($|[^\[(])").unwrap().replace_all(
let documentation = Regex::new(r"\[`([^`]*?)`]($|[^\[])").unwrap().replace_all(
documentation,
|caps: &Captures| {
format!(
@@ -137,7 +135,6 @@ fn process_documentation(documentation: &str, out: &mut String, rule_name: &str)
let anchor = option.replace('.', "_");
out.push_str(&format!("- [`{option}`][{option}]\n"));
after.push_str(&format!("[{option}]: ../settings.md#{anchor}\n"));
referenced_options.insert(option);
continue;
}
@@ -145,20 +142,6 @@ fn process_documentation(documentation: &str, out: &mut String, rule_name: &str)
out.push_str(line);
}
let re = Regex::new(r"\[`([^`]*?)`]\[(.*?)]").unwrap();
for (_, [option, _]) in re.captures_iter(&documentation).map(|c| c.extract()) {
if let Some(OptionEntry::Field(field)) = Options::metadata().find(option) {
if referenced_options.insert(option) {
let anchor = option.replace('.', "_");
after.push_str(&format!("[{option}]: ../settings.md#{anchor}\n"));
}
if field.deprecated.is_some() {
eprintln!("Rule {rule_name} references deprecated option {option}.");
}
}
}
if !after.is_empty() {
out.push('\n');
out.push('\n');
@@ -176,7 +159,7 @@ mod tests {
process_documentation(
"
See also [`lint.mccabe.max-complexity`] and [`lint.task-tags`].
Something [`else`][other]. Some [link](https://example.com).
Something [`else`][other].
## Options
@@ -191,7 +174,7 @@ Something [`else`][other]. Some [link](https://example.com).
output,
"
See also [`lint.mccabe.max-complexity`][lint.mccabe.max-complexity] and [`lint.task-tags`][lint.task-tags].
Something [`else`][other]. Some [link](https://example.com).
Something [`else`][other].
## Options

View File

@@ -180,22 +180,8 @@ pub(crate) fn generate() -> String {
.map(|rule| (rule.upstream_category(&linter), rule))
.into_group_map();
let mut rules_by_upstream_category: Vec<_> = rules_by_upstream_category.iter().collect();
// Sort the upstream categories alphabetically by prefix.
rules_by_upstream_category.sort_by(|(a, _), (b, _)| {
a.as_ref()
.map(|category| category.prefix)
.unwrap_or_default()
.cmp(
b.as_ref()
.map(|category| category.prefix)
.unwrap_or_default(),
)
});
if rules_by_upstream_category.len() > 1 {
for (opt, rules) in rules_by_upstream_category {
for (opt, rules) in &rules_by_upstream_category {
if opt.is_some() {
let UpstreamCategoryAndPrefix { category, prefix } = opt.unwrap();
table_out.push_str(&format!("#### {category} ({prefix})"));

View File

@@ -4,8 +4,8 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff::{args::GlobalConfigArgs, check};
use ruff_linter::logging::set_up_logging;
use ruff::check;
use ruff_linter::logging::{set_up_logging, LogLevel};
use std::process::ExitCode;
mod format_dev;
@@ -28,8 +28,6 @@ const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");
struct Args {
#[command(subcommand)]
command: Command,
#[clap(flatten)]
global_options: GlobalConfigArgs,
}
#[derive(Subcommand)]
@@ -59,6 +57,8 @@ enum Command {
Repeat {
#[clap(flatten)]
args: ruff::args::CheckCommand,
#[clap(flatten)]
log_level_args: ruff::args::LogLevelArgs,
/// Run this many times
#[clap(long)]
repeat: usize,
@@ -75,12 +75,9 @@ enum Command {
}
fn main() -> Result<ExitCode> {
let Args {
command,
global_options,
} = Args::parse();
let args = Args::parse();
#[allow(clippy::print_stdout)]
match command {
match args.command {
Command::GenerateAll(args) => generate_all::main(&args)?,
Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
@@ -92,12 +89,14 @@ fn main() -> Result<ExitCode> {
Command::PrintTokens(args) => print_tokens::main(&args)?,
Command::RoundTrip(args) => round_trip::main(&args)?,
Command::Repeat {
args: subcommand_args,
args,
repeat,
log_level_args,
} => {
set_up_logging(global_options.log_level())?;
let log_level = LogLevel::from(&log_level_args);
set_up_logging(&log_level)?;
for _ in 0..repeat {
check(subcommand_args.clone(), global_options.clone())?;
check(args.clone(), log_level)?;
}
}
Command::FormatDev(args) => {

View File

@@ -71,14 +71,6 @@ impl Diagnostic {
}
}
/// Consumes `self` and returns a new `Diagnostic` with the given parent node.
#[inline]
#[must_use]
pub fn with_parent(mut self, parent: TextSize) -> Self {
self.set_parent(parent);
self
}
/// Set the location of the diagnostic's parent node.
#[inline]
pub fn set_parent(&mut self, parent: TextSize) {

View File

@@ -25,6 +25,11 @@ pub trait Violation: Debug + PartialEq + Eq {
/// The message used to describe the violation.
fn message(&self) -> String;
/// The explanation used in documentation and elsewhere.
fn explanation() -> Option<&'static str> {
None
}
// TODO(micha): Move `fix_title` to `Fix`, add new `advice` method that is shown as an advice.
// Change the `Diagnostic` renderer to show the advice, and render the fix message after the `Suggested fix: <here>`
@@ -45,6 +50,11 @@ pub trait AlwaysFixableViolation: Debug + PartialEq + Eq {
/// The message used to describe the violation.
fn message(&self) -> String;
/// The explanation used in documentation and elsewhere.
fn explanation() -> Option<&'static str> {
None
}
/// The title displayed for the available fix.
fn fix_title(&self) -> String;
@@ -61,6 +71,10 @@ impl<V: AlwaysFixableViolation> Violation for V {
<Self as AlwaysFixableViolation>::message(self)
}
fn explanation() -> Option<&'static str> {
<Self as AlwaysFixableViolation>::explanation()
}
fn fix_title(&self) -> Option<String> {
Some(<Self as AlwaysFixableViolation>::fix_title(self))
}

View File

@@ -24,6 +24,7 @@ tracing = { workspace = true }
unicode-width = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
[features]
serde = ["dep:serde", "ruff_text_size/serde"]

View File

@@ -37,7 +37,7 @@ pub trait Buffer {
#[doc(hidden)]
fn elements(&self) -> &[FormatElement];
/// Glue for usage of the [`write!`] macro with implementers of this trait.
/// Glue for usage of the [`write!`] macro with implementors of this trait.
///
/// This method should generally not be invoked manually, but rather through the [`write!`] macro itself.
///

View File

@@ -138,7 +138,7 @@ pub const fn empty_line() -> Line {
///
/// # Examples
///
/// The line breaks are emitted as spaces if the enclosing `Group` fits on a single line:
/// The line breaks are emitted as spaces if the enclosing `Group` fits on a a single line:
/// ```
/// use ruff_formatter::{format, format_args};
/// use ruff_formatter::prelude::*;

View File

@@ -545,10 +545,6 @@ impl PrintedRange {
&self.code
}
pub fn into_code(self) -> String {
self.code
}
/// The range the formatted code corresponds to in the source document.
pub fn source_range(&self) -> TextRange {
self.source_range

View File

@@ -78,28 +78,27 @@ impl<'a> PrintQueue<'a> {
impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?;
elements.next().or_else(
#[cold]
|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
},
)
elements.next().or_else(|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
})
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?;
slice.as_slice().first().or_else(
#[cold]
|| {
slices
.next()
.and_then(|next_elements| next_elements.as_slice().first())
},
)
match slice.as_slice().first() {
Some(element) => Some(element),
None => {
if let Some(next_elements) = slices.next() {
next_elements.as_slice().first()
} else {
None
}
}
}
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
@@ -147,30 +146,24 @@ impl<'a, 'print> FitsQueue<'a, 'print> {
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else(
#[cold]
|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
},
)
self.queue.pop().or_else(|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
})
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else(
#[cold]
|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
},
)
self.queue.top_with_interned().or_else(|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
})
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.4.2"
version = "0.2.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -15,6 +15,7 @@ license = { workspace = true }
[dependencies]
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
@@ -59,6 +60,7 @@ regex = { workspace = true }
result-like = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true }
@@ -74,9 +76,11 @@ url = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
pretty_assertions = { workspace = true }
test-case = { workspace = true }
# Disable colored output in tests
colored = { workspace = true, features = ["no-color"] }
tempfile = { workspace = true }
[features]
default = []

View File

@@ -28,40 +28,3 @@ dictionary = {
}
#import os # noqa
# case 1:
# try:
# try: # with comment
# try: print()
# except:
# except Foo:
# except Exception as e: print(e)
# Script tag without an opening tag (Error)
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
# Script tag (OK)
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
# Script tag without a closing tag (OK)
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]

View File

@@ -18,7 +18,3 @@ func("0.0.0.0")
def my_func():
x = "0.0.0.0"
print(x)
# Implicit string concatenation
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0"

View File

@@ -18,13 +18,6 @@ with open("/dev/shm/unit/test", "w") as f:
with open("/foo/bar", "w") as f:
f.write("def")
# Implicit string concatenation
with open("/tmp/" "abc", "w") as f:
f.write("def")
with open("/tmp/abc" f"/tmp/abc", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory

View File

@@ -17,9 +17,3 @@ urllib.request.URLopener().open(fullurl='http://www.google.com')
urllib.request.URLopener().open('http://www.google.com')
urllib.request.URLopener().open('file:///foo/bar/baz')
urllib.request.URLopener().open(url)
urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
urllib.request.urlopen(urllib.request.Request(url))

View File

@@ -1,22 +0,0 @@
import os
import random
import a_lib
# OK
random.SystemRandom()
# Errors
random.Random()
random.random()
random.randrange()
random.randint()
random.choice()
random.choices()
random.uniform()
random.triangular()
random.randbytes()
# Unrelated
os.urandom()
a_lib.random()

View File

@@ -1,47 +1,52 @@
import crypt
import hashlib
from hashlib import new as hashlib_new
from hashlib import sha1 as hashlib_sha1
# Errors
# Invalid
hashlib.new('md5')
hashlib.new('md4', b'test')
hashlib.new(name='md5', data=b'test')
hashlib.new('MD4', data=b'test')
hashlib.new('sha1')
hashlib.new('sha1', data=b'test')
hashlib.new('sha', data=b'test')
hashlib.new(name='SHA', data=b'test')
hashlib.sha(data=b'test')
hashlib.md5()
hashlib_new('sha1')
hashlib_sha1('sha1')
# usedforsecurity arg only available in Python 3.9+
hashlib.new('sha1', usedforsecurity=True)
crypt.crypt("test", salt=crypt.METHOD_CRYPT)
crypt.crypt("test", salt=crypt.METHOD_MD5)
crypt.crypt("test", salt=crypt.METHOD_BLOWFISH)
crypt.crypt("test", crypt.METHOD_BLOWFISH)
# Valid
crypt.mksalt(crypt.METHOD_CRYPT)
crypt.mksalt(crypt.METHOD_MD5)
crypt.mksalt(crypt.METHOD_BLOWFISH)
# OK
hashlib.new('sha256')
hashlib.new('SHA512')
hashlib.sha256(data=b'test')
# usedforsecurity arg only available in Python 3.9+
hashlib_new(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib_sha1(name='sha1', usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.md4(usedforsecurity=False)
# usedforsecurity arg only available in Python 3.9+
hashlib.new(name='sha256', usedforsecurity=False)
crypt.crypt("test")
crypt.crypt("test", salt=crypt.METHOD_SHA256)
crypt.crypt("test", salt=crypt.METHOD_SHA512)
crypt.mksalt()
crypt.mksalt(crypt.METHOD_SHA256)
crypt.mksalt(crypt.METHOD_SHA512)

View File

@@ -1,5 +1,4 @@
import os
import subprocess
import commands
import popen2
@@ -17,8 +16,6 @@ popen2.Popen3("true")
popen2.Popen4("true")
commands.getoutput("true")
commands.getstatusoutput("true")
subprocess.getoutput("true")
subprocess.getstatusoutput("true")
# Check command argument looks unsafe.

View File

@@ -1,34 +0,0 @@
from django.contrib.auth.models import User
# Errors
User.objects.filter(username='admin').extra(dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select=dict(could_be='insecure'))
User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'})
User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')})
User.objects.filter(username='admin').extra(where=['%secure' % 'nos'])
User.objects.filter(username='admin').extra(where=['{}secure'.format('no')])
query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --'
User.objects.filter(username='admin').extra(select={'test': query})
where_var = ['1=1) OR 1=1 AND (1=1']
User.objects.filter(username='admin').extra(where=where_var)
where_str = '1=1) OR 1=1 AND (1=1'
User.objects.filter(username='admin').extra(where=[where_str])
tables_var = ['django_content_type" WHERE "auth_user"."username"="admin']
User.objects.all().extra(tables=tables_var).distinct()
tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
User.objects.all().extra(tables=[tables_str]).distinct()
# OK
User.objects.filter(username='admin').extra(
select={'test': 'secure'},
where=['secure'],
tables=['secure']
)
User.objects.filter(username='admin').extra({'test': 'secure'})
User.objects.filter(username='admin').extra(select={'test': 'secure'})
User.objects.filter(username='admin').extra(where=['secure'])

View File

@@ -124,8 +124,3 @@ try:
pass
except Exception:
error("...", exc_info=True)
try:
...
except Exception as e:
raise ValueError from e

View File

@@ -31,7 +31,8 @@ def function(
kwonly_nonboolvalued_boolhint: bool = 1,
kwonly_nonboolvalued_boolstrhint: "bool" = 1,
**kw,
): ...
):
...
def used(do):
@@ -118,39 +119,3 @@ def func(x: bool):
settings(True)
from dataclasses import dataclass, InitVar
@dataclass
class Fit:
force: InitVar[bool] = False
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)
# https://github.com/astral-sh/ruff/issues/10356
from django.db.models import Case, Q, Value, When
qs.annotate(
is_foo_or_bar=Case(
When(Q(is_foo=True) | Q(is_bar=True)),
then=Value(True),
),
default=Value(False),
)
# https://github.com/astral-sh/ruff/issues/10485
from pydantic import Field
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
foo: bool = Field(True, exclude=True)

View File

@@ -6,25 +6,6 @@ def this_is_a_bug():
print("Ooh, callable! Or is it?")
def still_a_bug():
import builtins
o = object()
if builtins.hasattr(o, "__call__"):
print("B U G")
if builtins.getattr(o, "__call__", False):
print("B U G")
def trickier_fix_for_this_one():
o = object()
def callable(x):
return True
if hasattr(o, "__call__"):
print("STILL a bug!")
def this_is_fine():
o = object()
if callable(o):

View File

@@ -1,20 +0,0 @@
def foo(a: list = []):
raise NotImplementedError("")
def bar(a: dict = {}):
""" This one also has a docstring"""
raise NotImplementedError("and has some text in here")
def baz(a: list = []):
"""This one raises a different exception"""
raise IndexError()
def qux(a: list = []):
raise NotImplementedError
def quux(a: list = []):
raise NotImplemented

View File

@@ -23,15 +23,3 @@ def okay(data: custom.ImmutableTypeA = foo()):
def error_due_to_missing_import(data: List[str] = Depends(None)):
...
class Class:
pass
def okay(obj=Class()):
...
def error(obj=OtherClass()):
...

View File

@@ -64,6 +64,3 @@ setattr(*foo, "bar", None)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1739800901
getattr(self.
registration.registry, '__name__')
import builtins
builtins.getattr(foo, "bar")

View File

@@ -9,69 +9,62 @@ B030:
try:
pass
except 1: # Error
except 1: # error
pass
try:
pass
except (1, ValueError): # Error
except (1, ValueError): # error
pass
try:
pass
except (ValueError, (RuntimeError, (KeyError, TypeError))): # Error
except (ValueError, (RuntimeError, (KeyError, TypeError))): # error
pass
try:
pass
except (ValueError, *(RuntimeError, (KeyError, TypeError))): # Error
except (ValueError, *(RuntimeError, (KeyError, TypeError))): # error
pass
try:
pass
except (*a, *(RuntimeError, (KeyError, TypeError))): # Error
except (*a, *(RuntimeError, (KeyError, TypeError))): # error
pass
try:
pass
except (ValueError, *(RuntimeError, TypeError)): # ok
pass
try:
pass
except (ValueError, *[RuntimeError, *(TypeError,)]): # ok
pass
try:
pass
except* a + (RuntimeError, (KeyError, TypeError)): # Error
except (*a, *b): # ok
pass
try:
pass
except (ValueError, *(RuntimeError, TypeError)): # OK
pass
try:
pass
except (ValueError, *[RuntimeError, *(TypeError,)]): # OK
except (*a, *(RuntimeError, TypeError)): # ok
pass
try:
pass
except (*a, *b): # OK
except (*a, *(b, c)): # ok
pass
try:
pass
except (*a, *(RuntimeError, TypeError)): # OK
pass
try:
pass
except (*a, *(b, c)): # OK
pass
try:
pass
except (*a, *(*b, *c)): # OK
except (*a, *(*b, *c)): # ok
pass
@@ -81,52 +74,5 @@ def what_to_catch():
try:
pass
except what_to_catch(): # OK
pass
try:
pass
except (a, b) + (c, d): # OK
pass
try:
pass
except* (a, b) + (c, d): # OK
pass
try:
pass
except* (a, (b) + (c)): # OK
pass
try:
pass
except (a, b) + (c, d) + (e, f): # OK
pass
try:
pass
except a + (b, c): # OK
pass
try:
pass
except (ValueError, *(RuntimeError, TypeError), *((ArithmeticError,) + (EOFError,))):
pass
try:
pass
except ((a, b) + (c, d)) + ((e, f) + (g)): # OK
pass
try:
pass
except (a, b) * (c, d): # B030
except what_to_catch(): # ok
pass

View File

@@ -174,49 +174,6 @@ for (_key1, _key2), (_value1, _value2) in groupby(
collect_shop_items("Jane", group[1])
collect_shop_items("Joe", group[1])
# Shouldn't trigger the warning when there is a continue, break statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
continue
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
break
collect_shop_items(shopper, section_items)
# Shouldn't trigger the warning when there is a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
return section_items
collect_shop_items(shopper, section_items)
# Should trigger the warning for duplicate access, even if is a return statement after.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
return
# Should trigger the warning for duplicate access, even if is a return in another branch.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items)
# Should trigger, since only one branch has a return statement.
for _section, section_items in groupby(items, key=lambda p: p[1]):
if _section == "greens":
collect_shop_items(shopper, section_items)
return
elif _section == "frozen items":
collect_shop_items(shopper, section_items)
collect_shop_items(shopper, section_items) # B031
# Let's redefine the `groupby` function to make sure we pick up the correct one.
# NOTE: This should always be at the end of the file.

View File

@@ -23,7 +23,3 @@ zip([1, 2, 3], repeat(1, times=None))
# Errors (limited iterators).
zip([1, 2, 3], repeat(1, 1))
zip([1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.zip([1, 2, 3])

View File

@@ -1,160 +0,0 @@
"""
Should emit:
B909 - on lines 11, 25, 26, 40, 46
"""
# lists
some_list = [1, 2, 3]
some_other_list = [1, 2, 3]
for elem in some_list:
# errors
some_list.remove(0)
del some_list[2]
some_list.append(elem)
some_list.sort()
some_list.reverse()
some_list.clear()
some_list.extend([1, 2])
some_list.insert(1, 1)
some_list.pop(1)
some_list.pop()
# conditional break should error
if elem == 2:
some_list.remove(0)
if elem == 3:
break
# non-errors
some_other_list.remove(elem)
del some_list
del some_other_list
found_idx = some_list.index(elem)
some_list = 3
# unconditional break should not error
if elem == 2:
some_list.remove(elem)
break
# dicts
mydicts = {"a": {"foo": 1, "bar": 2}}
for elem in mydicts:
# errors
mydicts.popitem()
mydicts.setdefault("foo", 1)
mydicts.update({"foo": "bar"})
# no errors
elem.popitem()
elem.setdefault("foo", 1)
elem.update({"foo": "bar"})
# sets
myset = {1, 2, 3}
for _ in myset:
# errors
myset.update({4, 5})
myset.intersection_update({4, 5})
myset.difference_update({4, 5})
myset.symmetric_difference_update({4, 5})
myset.add(4)
myset.discard(3)
# no errors
del myset
# members
class A:
some_list: list
def __init__(self, ls):
self.some_list = list(ls)
a = A((1, 2, 3))
# ensure member accesses are handled as errors
for elem in a.some_list:
a.some_list.remove(0)
del a.some_list[2]
# Augassign should error
foo = [1, 2, 3]
bar = [4, 5, 6]
for _ in foo:
foo *= 2
foo += bar
foo[1] = 9
foo[1:2] = bar
foo[1:2:3] = bar
foo = {1, 2, 3}
bar = {4, 5, 6}
for _ in foo: # should error
foo |= bar
foo &= bar
foo -= bar
foo ^= bar
# more tests for unconditional breaks - should not error
for _ in foo:
foo.remove(1)
for _ in bar:
bar.remove(1)
break
break
# should not error
for _ in foo:
foo.remove(1)
for _ in bar:
...
break
# should error (?)
for _ in foo:
foo.remove(1)
if bar:
bar.remove(1)
break
break
# should error
for _ in foo:
if bar:
pass
else:
foo.remove(1)
# should error
for elem in some_list:
if some_list.pop() == 2:
pass
# should not error
for elem in some_list:
if some_list.pop() == 2:
break
# should error
for elem in some_list:
if some_list.pop() == 2:
pass
else:
break
# should not error
for elem in some_list:
del some_list[elem]
some_list[elem] = 1
some_list.remove(elem)
some_list.discard(elem)

View File

@@ -1,20 +1,11 @@
# Cannot combine with C416. Should use list comprehension here.
even_nums = list(2 * x for x in range(3))
odd_nums = list(
2 * x + 1 for x in range(3)
)
# Short-circuit case, combine with C416 and should produce x = list(range(3))
x = list(x for x in range(3))
x = list(
x for x in range(3)
)
# Not built-in list.
def list(*args, **kwargs):
return None
list(2 * x for x in range(3))
list(x for x in range(3))

View File

@@ -1,30 +1,20 @@
# Cannot conbime with C416. Should use set comprehension here.
even_nums = set(2 * x for x in range(3))
odd_nums = set(
2 * x + 1 for x in range(3)
)
small_nums = f"{set(a if a < 6 else 0 for a in range(3))}"
x = set(x for x in range(3))
x = set(x for x in range(3))
y = f"{set(a if a < 6 else 0 for a in range(3))}"
_ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
print(f"Hello {set(a for a in range(3))} World")
def f(x):
return x
print(f'Hello {set(a for a in "abc")} World')
print(f"Hello {set(a for a in 'abc')} World")
print(f"Hello {set(f(a) for a in 'abc')} World")
print(f"Hello { set(f(a) for a in 'abc') } World")
# Short-circuit case, combine with C416 and should produce x = set(range(3))
x = set(x for x in range(3))
x = set(
x for x in range(3)
)
print(f"Hello {set(a for a in range(3))} World")
print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
# Not built-in set.
def set(*args, **kwargs):
return None
set(2 * x for x in range(3))
set(x for x in range(3))
# The fix generated for this diagnostic is incorrect, as we add additional space
# around the set comprehension.
print(f"{ {set(a for a in 'abc')} }")

View File

@@ -16,11 +16,3 @@ tuple( # comment
tuple([ # comment
1, 2
])
tuple((
1,
))
t6 = tuple([1])
t7 = tuple((1,))
t8 = tuple([1,])

View File

@@ -14,6 +14,9 @@ reversed(sorted(x, reverse=not x))
reversed(sorted(i for i in range(42)))
reversed(sorted((i for i in range(42)), reverse=True))
# Regression test for: https://github.com/astral-sh/ruff/issues/10335
reversed(sorted([1, 2, 3], reverse=False or True))
reversed(sorted([1, 2, 3], reverse=(False or True)))
def reversed(*args, **kwargs):
return None
reversed(sorted(x, reverse=True))

View File

@@ -13,10 +13,6 @@ all(x.id for x in bar)
all(x.id for x in bar)
any(x.id for x in bar)
all((x.id for x in bar))
# we don't lint on these in stable yet
sum([x.val for x in bar])
min([x.val for x in bar])
max([x.val for x in bar])
async def f() -> bool:

View File

@@ -1,8 +0,0 @@
sum([x.val for x in bar])
min([x.val for x in bar])
max([x.val for x in bar])
# Ok
sum(x.val for x in bar)
min(x.val for x in bar)
max(x.val for x in bar)

View File

@@ -1,3 +0,0 @@
# no lint if shadowed
def all(x): pass
all([x.id for x in bar])

View File

@@ -33,9 +33,3 @@ from datetime import datetime
# no replace orastimezone unqualified
datetime.strptime("something", "something")
# F-strings
datetime.strptime("something", f"%Y-%m-%dT%H:%M:%S{('.%f' if millis else '')}%z")
datetime.strptime("something", f"%Y-%m-%d %H:%M:%S%z")
# F-string is implicitly concatenated to another string
datetime.strptime("something", f"%Y-%m-%dT%H:%M:%S{('.%f' if millis else '')}" "%z")

View File

@@ -7,19 +7,7 @@ from pdb import set_trace as st
from celery.contrib.rdb import set_trace
from celery.contrib import rdb
import celery.contrib.rdb
from debugpy import wait_for_client
import debugpy
from ptvsd import break_into_debugger
from ptvsd import enable_attach
from ptvsd import wait_for_attach
import ptvsd
breakpoint()
st()
set_trace()
debugpy.breakpoint()
wait_for_client()
debugpy.listen(1234)
enable_attach()
break_into_debugger()
wait_for_attach()

View File

@@ -21,7 +21,6 @@ def unconventional_aliases():
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt

View File

@@ -1,10 +0,0 @@
def no_alias():
from django.conf import settings
def conventional_alias():
from django.conf import settings as settings
def unconventional_alias():
from django.conf import settings as s

View File

@@ -1,12 +1,9 @@
def func():
import logging
import logging
logging.WARN # LOG009
logging.WARNING # OK
logging.WARN # LOG009
logging.WARNING # OK
from logging import WARN, WARNING
def func():
from logging import WARN, WARNING
WARN # LOG009
WARNING # OK
WARN # LOG009
WARNING # OK

View File

@@ -227,11 +227,3 @@ class Repro[int](Protocol):
def impl(self) -> str:
"""Docstring"""
return self.func()
import typing
if typing.TYPE_CHECKING:
def contains_meaningful_ellipsis() -> list[int]:
"""Allow this in a TYPE_CHECKING block."""
...

View File

@@ -1,9 +1,6 @@
# PIE808
range(0, 10)
import builtins
builtins.range(0, 10)
# OK
range(x, 10)
range(-15, 10)

View File

@@ -1,14 +0,0 @@
"""Tests to ensure we correctly rename references inside `__all__`"""
from collections.abc import Set
__all__ = ["Set"]
if True:
__all__ += [r'''Set''']
if 1:
__all__ += ["S" "e" "t"]
if not False:
__all__ += ["Se" 't']

View File

@@ -1,14 +0,0 @@
"""Tests to ensure we correctly rename references inside `__all__`"""
from collections.abc import Set
__all__ = ["Set"]
if True:
__all__ += [r'''Set''']
if 1:
__all__ += ["S" "e" "t"]
if not False:
__all__ += ["Se" 't']

View File

@@ -1,6 +0,0 @@
"""
Tests for PYI025 where the import is marked as re-exported
through usage of a "redundant" `import Set as Set` alias
"""
from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe

View File

@@ -1,6 +0,0 @@
"""
Tests for PYI025 where the import is marked as re-exported
through usage of a "redundant" `import Set as Set` alias
"""
from collections.abc import Set as Set # PYI025 triggered but fix is not marked as safe

View File

@@ -14,15 +14,6 @@ IntOrStr: TypeAlias = int | str
IntOrFloat: Foo = int | float
AliasNone: typing.TypeAlias = None
class NotAnEnum:
NOT_A_STUB_SO_THIS_IS_FINE = None
from enum import Enum
class FooEnum(Enum): ...
class BarEnum(FooEnum):
BAR = None
# these are ok
VarAlias = str
AliasFoo = Foo

View File

@@ -13,16 +13,6 @@ IntOrStr: TypeAlias = int | str
IntOrFloat: Foo = int | float
AliasNone: typing.TypeAlias = None
class NotAnEnum:
FLAG_THIS = None
# these are ok
from enum import Enum
class FooEnum(Enum): ...
class BarEnum(FooEnum):
BAR = None
VarAlias = str
AliasFoo = Foo

View File

@@ -195,13 +195,6 @@ class BadAsyncIterator(collections.abc.AsyncIterator[str]):
def __aiter__(self) -> typing.AsyncIterator[str]:
... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
class SubclassOfBadIterator3(BadIterator3):
def __iter__(self) -> Iterator[int]: # Y034
...
class SubclassOfBadAsyncIterator(BadAsyncIterator):
def __aiter__(self) -> collections.abc.AsyncIterator[str]: # Y034
...
class AsyncIteratorReturningAsyncIterable:
def __aiter__(self) -> AsyncIterable[str]:
@@ -232,11 +225,6 @@ class MetaclassInWhichSelfCannotBeUsed4(ABCMeta):
async def __aenter__(self) -> MetaclassInWhichSelfCannotBeUsed4: ...
def __isub__(self, other: MetaclassInWhichSelfCannotBeUsed4) -> MetaclassInWhichSelfCannotBeUsed4: ...
class SubclassOfMetaclassInWhichSelfCannotBeUsed(MetaclassInWhichSelfCannotBeUsed4):
def __new__(cls) -> SubclassOfMetaclassInWhichSelfCannotBeUsed: ...
def __enter__(self) -> SubclassOfMetaclassInWhichSelfCannotBeUsed: ...
async def __aenter__(self) -> SubclassOfMetaclassInWhichSelfCannotBeUsed: ...
def __isub__(self, other: SubclassOfMetaclassInWhichSelfCannotBeUsed) -> SubclassOfMetaclassInWhichSelfCannotBeUsed: ...
class Abstract(Iterator[str]):
@abstractmethod

View File

@@ -3,7 +3,7 @@ import types
import typing
from collections.abc import Awaitable
from types import TracebackType
from typing import Any, Type, overload
from typing import Any, Type
import _typeshed
import typing_extensions
@@ -73,97 +73,3 @@ class BadFive:
class BadSix:
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
class AllPositionalOnlyArgs:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
class BadAllPositionalOnlyArgs:
def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ...
# Definitions not in a class scope can do whatever, we don't care
def __exit__(self, *args: bool) -> None: ...
async def __aexit__(self, *, go_crazy: bytes) -> list[str]: ...
# Here come the overloads...
class AcceptableOverload1:
@overload
def __exit__(self, exc_typ: None, exc: None, exc_tb: None) -> None: ...
@overload
def __exit__(self, exc_typ: type[BaseException], exc: BaseException, exc_tb: TracebackType) -> None: ...
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, exc_tb: TracebackType | None) -> None: ...
# Using `object` or `Unused` in an overload definition is kinda strange,
# but let's allow it to be on the safe side
class AcceptableOverload2:
@overload
def __exit__(self, exc_typ: None, exc: None, exc_tb: object) -> None: ...
@overload
def __exit__(self, exc_typ: Unused, exc: BaseException, exc_tb: object) -> None: ...
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, exc_tb: TracebackType | None) -> None: ...
class AcceptableOverload3:
# Just ignore any overloads that don't have exactly 3 annotated non-self parameters.
# We don't have the ability (yet) to do arbitrary checking
# of whether one function definition is a subtype of another...
@overload
def __exit__(self, exc_typ: bool, exc: bool, exc_tb: bool, weird_extra_arg: bool) -> None: ...
@overload
def __exit__(self, *args: object) -> None: ...
def __exit__(self, *args: object) -> None: ...
@overload
async def __aexit__(self, exc_typ: bool, /, exc: bool, exc_tb: bool, *, keyword_only: str) -> None: ...
@overload
async def __aexit__(self, *args: object) -> None: ...
async def __aexit__(self, *args: object) -> None: ...
class AcceptableOverload4:
# Same as above
@overload
def __exit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType) -> None: ...
@overload
def __exit__(self, *args: object) -> None: ...
def __exit__(self, *args: object) -> None: ...
@overload
async def __aexit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType, *, extra: str = "foo") -> None: ...
@overload
async def __aexit__(self, exc_typ: None, exc: None, tb: None) -> None: ...
async def __aexit__(self, *args: object) -> None: ...
class StrangeNumberOfOverloads:
# Only one overload? Type checkers will emit an error, but we should just ignore it
@overload
def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
# More than two overloads? Anything could be going on; again, just ignore all the overloads
@overload
async def __aexit__(self, arg: bool) -> None: ...
@overload
async def __aexit__(self, arg: None, arg2: None, arg3: None) -> None: ...
@overload
async def __aexit__(self, arg: bool, arg2: bool, arg3: bool) -> None: ...
async def __aexit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
# TODO: maybe we should emit an error on this one as well?
class BizarreAsyncSyncOverloadMismatch:
@overload
def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
@overload
async def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
def __exit__(self, *args: object) -> None: ...
class UnacceptableOverload1:
@overload
def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay
@overload
def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
class UnacceptableOverload2:
@overload
def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036
@overload
def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036
def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...

View File

@@ -3,7 +3,7 @@ import types
import typing
from collections.abc import Awaitable
from types import TracebackType
from typing import Any, Type, overload
from typing import Any, Type
import _typeshed
import typing_extensions
@@ -73,93 +73,3 @@ class BadFive:
class BadSix:
def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default
async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default
def isolated_scope():
from builtins import type as Type
class ShouldNotError:
def __exit__(self, typ: Type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
class AllPositionalOnlyArgs:
def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
class BadAllPositionalOnlyArgs:
def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ...
async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ...
# Definitions not in a class scope can do whatever, we don't care
def __exit__(self, *args: bool) -> None: ...
async def __aexit__(self, *, go_crazy: bytes) -> list[str]: ...
# Here come the overloads...
class AcceptableOverload1:
@overload
def __exit__(self, exc_typ: None, exc: None, exc_tb: None) -> None: ...
@overload
def __exit__(self, exc_typ: type[BaseException], exc: BaseException, exc_tb: TracebackType) -> None: ...
# Using `object` or `Unused` in an overload definition is kinda strange,
# but let's allow it to be on the safe side
class AcceptableOverload2:
@overload
def __exit__(self, exc_typ: None, exc: None, exc_tb: object) -> None: ...
@overload
def __exit__(self, exc_typ: Unused, exc: BaseException, exc_tb: object) -> None: ...
class AcceptableOverload3:
# Just ignore any overloads that don't have exactly 3 annotated non-self parameters.
# We don't have the ability (yet) to do arbitrary checking
# of whether one function definition is a subtype of another...
@overload
def __exit__(self, exc_typ: bool, exc: bool, exc_tb: bool, weird_extra_arg: bool) -> None: ...
@overload
def __exit__(self, *args: object) -> None: ...
@overload
async def __aexit__(self, exc_typ: bool, /, exc: bool, exc_tb: bool, *, keyword_only: str) -> None: ...
@overload
async def __aexit__(self, *args: object) -> None: ...
class AcceptableOverload4:
# Same as above
@overload
def __exit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType) -> None: ...
@overload
def __exit__(self, *args: object) -> None: ...
@overload
async def __aexit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType, *, extra: str = "foo") -> None: ...
@overload
async def __aexit__(self, exc_typ: None, exc: None, tb: None) -> None: ...
class StrangeNumberOfOverloads:
# Only one overload? Type checkers will emit an error, but we should just ignore it
@overload
def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
# More than two overloads? Anything could be going on; again, just ignore all the overloads
@overload
async def __aexit__(self, arg: bool) -> None: ...
@overload
async def __aexit__(self, arg: None, arg2: None, arg3: None) -> None: ...
@overload
async def __aexit__(self, arg: bool, arg2: bool, arg3: bool) -> None: ...
# TODO: maybe we should emit an error on this one as well?
class BizarreAsyncSyncOverloadMismatch:
@overload
def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
@overload
async def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ...
class UnacceptableOverload1:
@overload
def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay
@overload
def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036
class UnacceptableOverload2:
@overload
def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036
@overload
def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036

View File

@@ -64,5 +64,3 @@ def not_warnings_dot_deprecated(
"Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053
)
def not_a_deprecated_function() -> None: ...
fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053

View File

@@ -79,14 +79,5 @@ def test_single_list_of_lists(param):
@pytest.mark.parametrize("a", [1, 2])
@pytest.mark.parametrize(("b", "c"), ((3, 4), (5, 6)))
@pytest.mark.parametrize("d", [3,])
@pytest.mark.parametrize(
"d",
[("3", "4")],
)
@pytest.mark.parametrize(
"e",
[("3", "4"),],
)
def test_multiple_decorators(a, b, c, d, e):
def test_multiple_decorators(a, b, c):
pass

View File

@@ -51,8 +51,3 @@ def test_error_parentheses_trailing_comma(x):
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...
@pytest.mark.parametrize('data, spec', [(1.0, 1.0), (1.0, 1.0)])
def test_numbers(data, spec):
...

View File

@@ -1,9 +0,0 @@
class SingleLineDocstrings():
""'Start with empty string' ' and lint docstring safely'
""" Not a docstring """
def foo(self, bar="""not a docstring"""):
""'Start with empty string' ' and lint docstring safely'
pass
class Nested(foo()[:]): ""'Start with empty string' ' and lint docstring safely'; pass

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